├── .gitattributes
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── broken--documentation.md
│ ├── broken--fusionkit.md
│ ├── broken--library.md
│ ├── enhancement--documentation.md
│ ├── enhancement--fusionkit.md
│ ├── enhancement--library.md
│ └── meta.md
└── workflows
│ ├── ci.yml
│ └── mkdocs-deploy.yml
├── .gitignore
├── .luaurc
├── .vscode
└── Fusion.code-workspace
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── aftman.toml
├── default.project.json
├── docs
├── api-reference
│ ├── animation
│ │ ├── members
│ │ │ ├── spring.md
│ │ │ └── tween.md
│ │ └── types
│ │ │ ├── animatable.md
│ │ │ ├── spring.md
│ │ │ └── tween.md
│ ├── general
│ │ ├── errors.md
│ │ ├── members
│ │ │ ├── contextual.md
│ │ │ ├── safe.md
│ │ │ └── version.md
│ │ └── types
│ │ │ ├── contextual.md
│ │ │ └── version.md
│ ├── graph
│ │ ├── members
│ │ │ └── observer.md
│ │ └── types
│ │ │ ├── graphobject.md
│ │ │ └── observer.md
│ ├── index.md
│ ├── memory
│ │ ├── members
│ │ │ ├── derivescope.md
│ │ │ ├── docleanup.md
│ │ │ ├── innerscope.md
│ │ │ ├── insert.md
│ │ │ └── scoped.md
│ │ └── types
│ │ │ ├── scope.md
│ │ │ ├── scopedobject.md
│ │ │ └── task.md
│ ├── roblox
│ │ ├── members
│ │ │ ├── attribute.md
│ │ │ ├── attributechange.md
│ │ │ ├── attributeout.md
│ │ │ ├── child.md
│ │ │ ├── children.md
│ │ │ ├── hydrate.md
│ │ │ ├── new.md
│ │ │ ├── onchange.md
│ │ │ ├── onevent.md
│ │ │ └── out.md
│ │ └── types
│ │ │ ├── child.md
│ │ │ ├── propertytable.md
│ │ │ └── specialkey.md
│ └── state
│ │ ├── members
│ │ ├── computed.md
│ │ ├── forkeys.md
│ │ ├── forpairs.md
│ │ ├── forvalues.md
│ │ ├── peek.md
│ │ └── value.md
│ │ └── types
│ │ ├── computed.md
│ │ ├── for.md
│ │ ├── stateobject.md
│ │ ├── use.md
│ │ ├── usedas.md
│ │ └── value.md
├── assets
│ ├── 404-dark.svg
│ ├── 404-light.svg
│ ├── aura.png
│ ├── heroes
│ │ ├── api-reference.jpg
│ │ └── examples.jpg
│ ├── home
│ │ ├── Animation-Dark.svg
│ │ ├── Animation-Light.svg
│ │ ├── Hero-Dark.svg
│ │ ├── Hero-Light.svg
│ │ ├── Instances-Dark.svg
│ │ ├── Instances-Light.svg
│ │ ├── State-Dark.svg
│ │ ├── State-Light.svg
│ │ └── fusion-clip-shape.svg
│ ├── logo-dark.svg
│ ├── logo-light.svg
│ ├── overrides
│ │ ├── 404.html
│ │ ├── home.html
│ │ ├── main.html
│ │ └── partials
│ │ │ ├── content.html
│ │ │ ├── footer.html
│ │ │ ├── header.html
│ │ │ ├── logo.html
│ │ │ └── search.html
│ ├── scripts
│ │ ├── error-paste-box.js
│ │ └── smooth-scroll.js
│ ├── theme
│ │ ├── 404.css
│ │ ├── admonition.css
│ │ ├── api-reference.css
│ │ ├── code.css
│ │ ├── colours.css
│ │ ├── dev-tools.css
│ │ ├── fusiondoc.css
│ │ ├── home.css
│ │ ├── page.css
│ │ └── paragraph.css
│ ├── wip.svg
│ ├── wordmark-tiny-dark.svg
│ └── wordmark-tiny-light.svg
├── examples
│ ├── cookbook
│ │ ├── animated-computed.md
│ │ ├── button-component.md
│ │ ├── drag-and-drop.md
│ │ ├── fetch-data-from-server.md
│ │ ├── index.md
│ │ ├── light-and-dark-theme.md
│ │ ├── loading-spinner.md
│ │ └── player-list.md
│ ├── index.md
│ └── place-thumbnails
│ │ ├── Fusion-Obby.jpg
│ │ └── Fusion-Wordle.jpg
├── index.md
└── tutorials
│ ├── animation
│ ├── springs.md
│ ├── springs
│ │ ├── Damping-Critical-Dark.png
│ │ ├── Damping-Critical-Light.png
│ │ ├── Damping-Over-Dark.png
│ │ ├── Damping-Over-Light.png
│ │ ├── Damping-Under-Dark.png
│ │ ├── Damping-Under-Light.png
│ │ ├── Damping-Zero-Dark.png
│ │ ├── Damping-Zero-Light.png
│ │ ├── Following-Dark.png
│ │ ├── Following-Light.png
│ │ ├── Interrupted-Dark.png
│ │ ├── Interrupted-Light.png
│ │ ├── Speed-Dark.png
│ │ ├── Speed-Light.png
│ │ ├── Step-Basic-Dark.png
│ │ └── Step-Basic-Light.png
│ ├── tweens.md
│ └── tweens
│ │ ├── Delay-Dark.png
│ │ ├── Delay-Light.png
│ │ ├── Easing-Direction-Dark.png
│ │ ├── Easing-Direction-Light.png
│ │ ├── Easing-Style-Dark.png
│ │ ├── Easing-Style-Light.png
│ │ ├── Follow-Failure-Dark.png
│ │ ├── Follow-Failure-Light.png
│ │ ├── Interrupted-Dark.png
│ │ ├── Interrupted-Light.png
│ │ ├── Repeats-Dark.png
│ │ ├── Repeats-Light.png
│ │ ├── Reversing-Dark.png
│ │ ├── Reversing-Light.png
│ │ ├── Step-Basic-Dark.png
│ │ ├── Step-Basic-Light.png
│ │ ├── Time-Dark.png
│ │ └── Time-Light.png
│ ├── best-practices
│ ├── callbacks.md
│ ├── callbacks
│ │ ├── Top-Down-Control-Dark.svg
│ │ └── Top-Down-Control-Light.svg
│ ├── components.md
│ ├── error-safety.md
│ ├── instance-handling.md
│ ├── instance-handling
│ │ ├── Popup-Exploded-Dark.svg
│ │ ├── Popup-Exploded-Light.svg
│ │ ├── Popups-Dark.svg
│ │ └── Popups-Light.svg
│ ├── optimisation.md
│ ├── references.md
│ ├── sharing-values.md
│ ├── state.md
│ └── state
│ │ ├── Check-Boxes-Dark.svg
│ │ ├── Check-Boxes-Light.svg
│ │ ├── Master-Check-Box-Dark.svg
│ │ └── Master-Check-Box-Light.svg
│ ├── fundamentals
│ ├── computeds.md
│ ├── observers.md
│ ├── scopes.md
│ ├── values.md
│ └── your-first-project.md
│ ├── get-started
│ ├── developer-tools.md
│ ├── developer-tools
│ │ └── community
│ │ │ ├── codify.png
│ │ │ ├── flipbook.png
│ │ │ ├── hoarcekat.png
│ │ │ ├── lydie.png
│ │ │ ├── onyxui.png
│ │ │ └── rojo.png
│ ├── getting-help.md
│ ├── installing-fusion.md
│ └── installing-fusion
│ │ ├── Github-Releases-Guide-1-Dark.png
│ │ ├── Github-Releases-Guide-1-Light.png
│ │ ├── Github-Releases-Guide-2-Dark.png
│ │ ├── Github-Releases-Guide-2-Light.png
│ │ ├── Github-Releases-Guide-3-Dark.png
│ │ └── Github-Releases-Guide-3-Light.png
│ ├── index.md
│ ├── roblox
│ ├── change-events.md
│ ├── events.md
│ ├── hydration.md
│ ├── hydration
│ │ ├── Hydration-Basic-Dark.svg
│ │ └── Hydration-Basic-Light.svg
│ ├── new-instances.md
│ ├── new-instances
│ │ ├── Default-Props-Dark.svg
│ │ └── Default-Props-Light.svg
│ ├── outputs.md
│ └── parenting.md
│ └── tables
│ ├── forkeys.md
│ ├── forpairs.md
│ ├── forpairs
│ ├── Optimisation-KeyValueChange-Dark.svg
│ ├── Optimisation-KeyValueChange-Light.svg
│ ├── Optimisation-KeyValuePreserve-Dark.svg
│ └── Optimisation-KeyValuePreserve-Light.svg
│ ├── forvalues.md
│ └── forvalues
│ ├── Optimisation-Duplicates-Dark.svg
│ ├── Optimisation-Duplicates-Light.svg
│ ├── Optimisation-Reordering-Dark.svg
│ └── Optimisation-Reordering-Light.svg
├── gh-assets
├── clearfloat.svg
├── link-docs.svg
├── link-download.svg
├── logo-dark-theme.svg
└── logo-light-theme.svg
├── mkdocs.yml
├── package.json
├── selene.toml
├── src
├── Animation
│ ├── ExternalTime.luau
│ ├── Spring.luau
│ ├── Stopwatch.luau
│ ├── Tween.luau
│ ├── getTweenDuration.luau
│ ├── getTweenRatio.luau
│ ├── lerpType.luau
│ ├── packType.luau
│ ├── springCoefficients.luau
│ └── unpackType.luau
├── Colour
│ ├── Oklab.luau
│ └── sRGB.luau
├── External.luau
├── ExternalDebug.luau
├── Graph
│ ├── Observer.luau
│ ├── castToGraph.luau
│ ├── change.luau
│ ├── depend.luau
│ └── evaluate.luau
├── Instances
│ ├── Attribute.luau
│ ├── AttributeChange.luau
│ ├── AttributeOut.luau
│ ├── Child.luau
│ ├── Children.luau
│ ├── Hydrate.luau
│ ├── New.luau
│ ├── OnChange.luau
│ ├── OnEvent.luau
│ ├── Out.luau
│ ├── applyInstanceProps.luau
│ └── defaultProps.luau
├── Logging
│ ├── formatError.luau
│ ├── messages.luau
│ └── parseError.luau
├── Memory
│ ├── checkLifetime.luau
│ ├── deriveScope.luau
│ ├── deriveScopeImpl.luau
│ ├── doCleanup.luau
│ ├── innerScope.luau
│ ├── insert.luau
│ ├── needsDestruction.luau
│ └── scoped.luau
├── RobloxExternal.luau
├── State
│ ├── Computed.luau
│ ├── For
│ │ ├── Disassembly.luau
│ │ ├── ForTypes.luau
│ │ └── init.luau
│ ├── ForKeys.luau
│ ├── ForPairs.luau
│ ├── ForValues.luau
│ ├── Value.luau
│ ├── castToState.luau
│ ├── peek.luau
│ └── updateAll.luau
├── Types.luau
├── Utility
│ ├── Contextual.luau
│ ├── Safe.luau
│ ├── isSimilar.luau
│ ├── merge.luau
│ ├── nameOf.luau
│ ├── never.luau
│ ├── nicknames.luau
│ └── xtypeof.luau
└── init.luau
├── test-runner.project.json
├── test
├── Spec
│ ├── Animation
│ │ └── springCoefficients.spec.luau
│ ├── Graph
│ │ ├── Observer.spec.luau
│ │ ├── change.spec.luau
│ │ └── evaluate.spec.luau
│ ├── Instances
│ │ ├── Attribute.spec.luau
│ │ ├── AttributeChange.spec.luau
│ │ ├── AttributeOut.spec.luau
│ │ ├── Children.spec.luau
│ │ ├── Hydrate.spec.luau
│ │ ├── New.spec.luau
│ │ ├── OnChange.spec.luau
│ │ ├── OnEvent.spec.luau
│ │ ├── Out.spec.luau
│ │ └── applyInstanceProps.spec.luau
│ ├── Memory
│ │ ├── deriveScope.spec.luau
│ │ ├── doCleanup.spec.luau
│ │ ├── innerScope.spec.luau
│ │ ├── insert.spec.luau
│ │ └── scoped.spec.luau
│ ├── State
│ │ ├── Computed.spec.luau
│ │ ├── ForKeys.spec.luau
│ │ ├── ForPairs.spec.luau
│ │ ├── ForValues.spec.luau
│ │ └── Value.spec.luau
│ ├── Utility
│ │ ├── Contextual.spec.luau
│ │ ├── Safe.spec.luau
│ │ └── isSimilar.spec.luau
│ └── _Integration
│ │ └── DynamicGraphs.spec.lua
├── SpecExternal.luau
├── TestEZ
│ ├── Context.luau
│ ├── Expectation.luau
│ ├── ExpectationContext.luau
│ ├── LifecycleHooks.luau
│ ├── Reporters
│ │ ├── TeamCityReporter.luau
│ │ ├── TextReporter.luau
│ │ └── TextReporterQuiet.luau
│ ├── TestBootstrap.luau
│ ├── TestEnum.luau
│ ├── TestPlan.luau
│ ├── TestPlanner.luau
│ ├── TestResults.luau
│ ├── TestRunner.luau
│ ├── TestSession.luau
│ └── init.luau
├── TestVars.luau
├── Util
│ ├── FiniteTime.luau
│ └── Graphs.luau
└── init.server.luau
└── wally.toml
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [dphfox]
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/broken--documentation.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Broken: Documentation'
3 | about: Documentation inaccuracies or errors in examples
4 | title: ''
5 | labels: broken, not ready - evaluating
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/broken--fusionkit.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Broken: FusionKit'
3 | about: An official developer tool isn't working correctly
4 | title: ''
5 | labels: 'broken, not ready - evaluating, targeting: FusionKit'
6 | assignees: dphfox
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/broken--library.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Broken: Library'
3 | about: Unexpected errors or incorrect behaviour with the Fusion library
4 | title: ''
5 | labels: broken, not ready - evaluating
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/enhancement--documentation.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Enhancement: Documentation'
3 | about: More clarity, new examples, anything that isn't broken
4 | title: ''
5 | labels: 'enhancement, not ready - evaluating, targeting: docs'
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/enhancement--fusionkit.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Enhancement: FusionKit'
3 | about: New features or quality of life improvements for dev tools
4 | title: ''
5 | labels: 'enhancement, not ready - evaluating, targeting: FusionKit'
6 | assignees: dphfox
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/enhancement--library.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Enhancement: Library'
3 | about: Suggest a new feature for the Fusion library
4 | title: ''
5 | labels: enhancement, not ready - evaluating
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Meta
3 | about: Relating to the Fusion project, repository or management
4 | title: ''
5 | labels: 'targeting: meta'
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # Taken from roblox-ts under the MIT license https://github.com/roblox-ts/roblox-ts/blob/master/.github/workflows/ci.yml
2 |
3 | name: CI
4 |
5 | on:
6 | push:
7 | branches:
8 | main
9 |
10 | jobs:
11 | lint:
12 | name: Lint
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout repository
16 | uses: actions/checkout@v2.3.4
17 |
18 | - name: Install Aftman
19 | uses: ok-nick/setup-aftman@v0.4.2
20 | with:
21 | token: ${{ secrets.GITHUB_TOKEN }}
22 |
23 | - name: Run Selene
24 | run: selene src test
--------------------------------------------------------------------------------
/.github/workflows/mkdocs-deploy.yml:
--------------------------------------------------------------------------------
1 | name: MkDocs Deploy
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | env:
8 | FUSION_VERSION: 0.4
9 | FUSION_DEPLOY_TYPE: 'dev'
10 |
11 | jobs:
12 | deploy:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout repo
16 | uses: actions/checkout@v3
17 |
18 | - name: Install Python 3.x
19 | uses: actions/setup-python@v4
20 | with:
21 | python-version: 3.x
22 |
23 | - name: Install setuptools (for mike)
24 | run: pip install --upgrade setuptools
25 |
26 | - name: Install MkDocs
27 | run: pip install mkdocs-material==8.2.13
28 |
29 | - name: Install mike
30 | run: pip install mike==1.1.2
31 |
32 | - name: Configure FusionDoc Git user
33 | run: |
34 | git config user.name fusiondoc
35 | git config user.email fusiondoc@example.com
36 |
37 | - name: Fetch gh-pages branch
38 | run: git fetch origin gh-pages --depth=1
39 |
40 | - name: Deploy latest documentation
41 | if: env.FUSION_DEPLOY_TYPE == 'release'
42 | run: mike deploy $FUSION_VERSION latest -u -b gh-pages
43 |
44 | - name: Set default version to latest
45 | if: env.FUSION_DEPLOY_TYPE == 'release'
46 | run: mike set-default latest -b gh-pages -p
47 |
48 | - name: Deploy developer documentation
49 | if: env.FUSION_DEPLOY_TYPE == 'dev'
50 | run: mike deploy $FUSION_VERSION -u -b gh-pages -p
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore Roblox files
2 | *.rbxlx
3 | *.rbxmx
4 | *.rbxm
5 | *.rbxl
6 |
7 | # Ignore lock files
8 | *.lock
9 |
10 | # Ignore site folder
11 | /site/
12 |
13 | # Ignore selene auto-generated config
14 | roblox.toml
15 |
16 | # Ignore windows styling
17 | desktop.ini
18 |
19 | # Ignore sourcemap generated by Rojo
20 | # Regen with `rojo sourcemap -o sourcemap.json .\test-runner.project.json`
21 | sourcemap.json
22 |
23 | # Ignore MacOS DS_Store
24 | .DS_Store
25 |
--------------------------------------------------------------------------------
/.luaurc:
--------------------------------------------------------------------------------
1 | {
2 | "languageMode": "strict"
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/Fusion.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": ".."
5 | }
6 | ],
7 | "settings": {}
8 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Daniel P H Fox
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ### Futuristic Luau for every universe.
4 |
5 | Fusion is a portable Luau companion library for simpler, more descriptive code.
6 |
7 | With Fusion, assemble straightforward chains of logic that are easy to understand,
8 | predict and debug. Make strong guarantees about what your code will or won't do.
9 | Build joyfully custom-fitted APIs to interact with the world outside of your code.
10 |
11 | Fusion provides batteries-included configuration for Roblox, and fantastic extensibility
12 | and integration for anything else Luau.
13 |
14 | Piqued your interest? [Get going in minutes with our on-rails tutorial.](https://elttob.uk/Fusion/latest/tutorials)
15 |
16 | ## License
17 |
18 | Fusion is licensed freely under MIT. Go do cool stuff with it, and if you feel
19 | like it, give us a shoutout!
20 |
--------------------------------------------------------------------------------
/aftman.toml:
--------------------------------------------------------------------------------
1 | # This file lists tools managed by Aftman, a cross-platform toolchain manager.
2 | # For more information, see https://github.com/LPGhatguy/aftman
3 |
4 | # To add a new tool, add an entry to this table.
5 | [tools]
6 | rojo = "rojo-rbx/rojo@7.4.1"
7 | selene = "Kampfkarren/selene@0.26.1"
--------------------------------------------------------------------------------
/default.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Fusion",
3 | "tree": {
4 | "$path": "src"
5 | }
6 | }
--------------------------------------------------------------------------------
/docs/api-reference/animation/members/tween.md:
--------------------------------------------------------------------------------
1 |
2 | Animation
3 | Members
4 | Tween
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.Tween(
17 | scope: Scope,
18 | goal: UsedAs,
19 | tweenInfo: UsedAs?
20 | ) -> Tween
21 | ```
22 |
23 | Constructs and returns a new [tween state object](../../types/tween).
24 |
25 | !!! success "Use scoped() method syntax"
26 | This function is intended to be accessed as a method on a scope:
27 | ```Lua
28 | local tween = scope:Tween(goal, info)
29 | ```
30 |
31 | -----
32 |
33 | ## Parameters
34 |
35 |
36 | scope
37 |
38 | : Scope <S>
39 |
40 |
41 |
42 | The [scope](../../../memory/types/scope) which should be used to store
43 | destruction tasks for this object.
44 |
45 |
46 | goal
47 |
48 | : UsedAs <T>
49 |
50 |
51 |
52 | The goal that this object should follow. For best results, the goal should be
53 | [animatable](../../types/animatable).
54 |
55 |
56 | info
57 |
58 | : UsedAs <TweenInfo>?
59 |
60 |
61 |
62 | Determines the easing curve that the motion will follow.
63 |
64 | -----
65 |
66 |
67 | Returns
68 |
69 | -> Tween <T>
70 |
71 |
72 |
73 | A freshly constructed tween state object.
74 |
75 | -----
76 |
77 | ## Learn More
78 |
79 | - [Tweens tutorial](../../../../tutorials/animation/tweens)
--------------------------------------------------------------------------------
/docs/api-reference/animation/types/animatable.md:
--------------------------------------------------------------------------------
1 |
2 | Animation
3 | Types
4 | Animatable
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type Animatable =
14 | number |
15 | CFrame |
16 | Color3 |
17 | ColorSequenceKeypoint |
18 | DateTime |
19 | NumberRange |
20 | NumberSequenceKeypoint |
21 | PhysicalProperties |
22 | Ray |
23 | Rect |
24 | Region3 |
25 | Region3int16 |
26 | UDim |
27 | UDim2 |
28 | Vector2 |
29 | Vector2int16 |
30 | Vector3 |
31 | Vector3int16
32 | ```
33 |
34 | Any data type that Fusion can decompose into a tuple of animatable parameters.
35 |
36 | !!! note "Passing other types to animation objects"
37 | Other types can be passed to `Tween` and `Spring` objects, however those
38 | types will not animate. Instead, non-`Animatable` types will immediately
39 | arrive at their goal value.
40 |
41 | -----
42 |
43 | ## Learn More
44 |
45 | - [Tweens tutorial](../../../../tutorials/animation/tweens)
46 | - [Springs tutorial](../../../../tutorials/animation/springs)
--------------------------------------------------------------------------------
/docs/api-reference/animation/types/spring.md:
--------------------------------------------------------------------------------
1 |
2 | Animation
3 | Types
4 | Spring
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type Spring = StateObject & {
14 | kind: "Spring",
15 | setPosition: (self, newPosition: T) -> (),
16 | setVelocity: (self, newVelocity: T) -> (),
17 | addVelocity: (self, deltaVelocity: T) -> ()
18 | }
19 | ```
20 |
21 | A specialised [state object](../../../state/types/stateobject) for following a goal state smoothly
22 | over time, using physics to shape the motion.
23 |
24 | The methods on this type allow for direct control over the position and velocity
25 | of the motion. Other than that, this type is of limited utility outside of
26 | Fusion itself.
27 |
28 | -----
29 |
30 | ## Members
31 |
32 |
33 | kind
34 |
35 | : "Spring"
36 |
37 |
38 |
39 | A more specific type string which can be used for runtime type checking. This
40 | can be used to tell types of state object apart.
41 |
42 | -----
43 |
44 | ## Methods
45 |
46 |
47 | setPosition
48 |
49 | -> ()
50 |
51 |
52 |
53 | ```Lua
54 | function Spring:setPosition(
55 | newPosition: T
56 | ): ()
57 | ```
58 |
59 | Immediately snaps the spring to the given position. The position must have the
60 | same `typeof()` as the goal state.
61 |
62 |
63 | setVelocity
64 |
65 | -> ()
66 |
67 |
68 |
69 | ```Lua
70 | function Spring:setVelocity(
71 | newVelocity: T
72 | ): ()
73 | ```
74 |
75 | Overwrites the spring's velocity without changing its position. The velocity
76 | must have the same `typeof()` as the goal state.
77 |
78 |
79 | addVelocity
80 |
81 | -> ()
82 |
83 |
84 |
85 | ```Lua
86 | function Spring:addVelocity(
87 | deltaVelocity: T
88 | ): ()
89 | ```
90 |
91 | Appends to the spring's velocity without changing its position. The velocity
92 | must have the same `typeof()` as the goal state.
93 |
94 | -----
95 |
96 | ## Learn More
97 |
98 | - [Springs tutorial](../../../../tutorials/animation/springs)
--------------------------------------------------------------------------------
/docs/api-reference/animation/types/tween.md:
--------------------------------------------------------------------------------
1 |
2 | Animation
3 | Types
4 | Tween
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type Tween = StateObject & {
14 | kind: "Tween"
15 | }
16 | ```
17 |
18 | A specialised [state object](../../../state/types/stateobject) for following a goal state smoothly
19 | over time, using a `TweenInfo` to shape the motion.
20 |
21 | This type isn't generally useful outside of Fusion itself.
22 |
23 | -----
24 |
25 | ## Members
26 |
27 |
28 | kind
29 |
30 | : "Tween"
31 |
32 |
33 |
34 | A more specific type string which can be used for runtime type checking. This
35 | can be used to tell types of state object apart.
36 |
37 | -----
38 |
39 | ## Learn More
40 |
41 | - [Tweens tutorial](../../../../tutorials/animation/tweens)
--------------------------------------------------------------------------------
/docs/api-reference/general/members/contextual.md:
--------------------------------------------------------------------------------
1 |
2 | General
3 | Members
4 | Contextual
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.Contextual(
17 | defaultValue: T
18 | ): Contextual
19 | ```
20 |
21 | Constructs and returns a new [contextual](../../types/contextual).
22 |
23 | -----
24 |
25 | ## Parameters
26 |
27 |
28 | defaultValue
29 |
30 | : T
31 |
32 |
33 |
34 | The value which `Contextual:now()` should return if no value has been specified
35 | by `Contextual:is():during()`.
36 |
37 | -----
38 |
39 |
40 | Returns
41 |
42 | -> Contextual <T>
43 |
44 |
45 |
46 | A freshly constructed contextual.
47 |
48 | -----
49 |
50 | ## Learn More
51 |
52 | - [Sharing Values tutorial](../../../../tutorials/best-practices/sharing-values)
--------------------------------------------------------------------------------
/docs/api-reference/general/members/safe.md:
--------------------------------------------------------------------------------
1 |
2 | General
3 | Members
4 | Safe
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.Safe(
17 | callbacks: {
18 | try: () -> Success,
19 | fallback: (err: unknown) -> Fail
20 | }
21 | ): Success | Fail
22 | ```
23 |
24 | Safely runs a function and returns the value it produces. If the function fails,
25 | the `fallback` function can handle the error and produces a fallback value.
26 |
27 | `Safe` acts like a version of `xpcall` that is easier to use in calculations and
28 | expressions, because it only returns the values from the functions, rather than
29 | returning a success boolean.
30 |
31 | !!! warning "Fatal versus non-fatal errors"
32 | `Safe` only protects you from errors that would stop your calculation from
33 | successfully returning a value.
34 |
35 | In particular, this applies to [computeds](../../../state/members/computed)
36 | you create inside `Safe` (and other similar objects). Because errors there
37 | are safely handled by those objects, and do not cause the `Safe` calculation
38 | to crash fatally, you have to use `Safe` inside of the computed itself if
39 | you want to capture the error.
40 |
41 | -----
42 |
43 | ## Properties
44 |
45 |
46 | try
47 |
48 | : () -> Success
49 |
50 |
51 |
52 | The possibly erroneous calculation or expression.
53 |
54 |
55 | fallback
56 |
57 | : (err: unknown) -> Fail
58 |
59 |
60 |
61 | A fallback calculation that should provide a backup answer if the possibly
62 | erroneous calculation throws an error.
63 |
64 | -----
65 |
66 |
67 | Returns
68 |
69 | -> Success | Fail
70 |
71 |
72 |
73 | The value produced by `try` if it's successful, or the value produced by
74 | `fallback` if an error occurs during `try`.
--------------------------------------------------------------------------------
/docs/api-reference/general/members/version.md:
--------------------------------------------------------------------------------
1 |
2 | General
3 | Members
4 | version
5 |
6 |
7 |
14 |
15 | ```Lua
16 | Fusion.version: Version
17 | ```
18 |
19 | The version of the Fusion source code.
20 |
21 | `isRelease` is only `true` when using a version of Fusion downloaded from
22 | [the Releases page](https://github.com/dphfox/Fusion/releases).
--------------------------------------------------------------------------------
/docs/api-reference/general/types/contextual.md:
--------------------------------------------------------------------------------
1 |
2 | General
3 | Types
4 | Contextual
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type Contextual = {
14 | type: "Contextual",
15 | now: (self) -> T,
16 | is: (self, newValue: T) -> {
17 | during: (self, callback: (A...) -> R, A...) -> R
18 | }
19 | }
20 | ```
21 |
22 | An object representing a widely-accessible value, which can take on different
23 | values at different times in different coroutines.
24 |
25 | !!! note "Non-standard type syntax"
26 | The above type definition uses `self` to denote methods. At time of writing,
27 | Luau does not interpret `self` specially.
28 |
29 | -----
30 |
31 | ## Fields
32 |
33 |
34 | type
35 |
36 | : "Contextual"
37 |
38 |
39 |
40 | A type string which can be used for runtime type checking.
41 |
42 | -----
43 |
44 | ## Methods
45 |
46 |
47 | now
48 |
49 | -> T
50 |
51 |
52 |
53 | ```Lua
54 | function Contextual:now(): T
55 | ```
56 |
57 | Returns the current value of this contextual. This varies based on when the
58 | function is called, and in what coroutine it was called.
59 |
60 |
61 | is/during
62 |
63 | -> R
64 |
65 |
66 |
67 | ```Lua
68 | function Contextual:is(
69 | newValue: T
70 | ): {
71 | during: (
72 | self,
73 | callback: (A...) -> R,
74 | A...
75 | ) -> R
76 | }
77 | ```
78 |
79 | Runs the `callback` with the arguments `A...` and returns the value the callback
80 | returns (`R`). The `Contextual` will appear to be `newValue` in the callback,
81 | unless it's overridden by another `:is():during()` call.
82 |
83 | -----
84 |
85 | ## Learn More
86 |
87 | - [Sharing Values tutorial](../../../../tutorials/best-practices/sharing-values)
--------------------------------------------------------------------------------
/docs/api-reference/general/types/version.md:
--------------------------------------------------------------------------------
1 |
2 | General
3 | Types
4 | Version
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type Version = {
14 | major: number,
15 | minor: number,
16 | isRelease: boolean
17 | }
18 | ```
19 |
20 | Describes a version of Fusion's source code.
21 |
22 | -----
23 |
24 | ## Members
25 |
26 |
27 | major
28 |
29 | : number
30 |
31 |
32 |
33 | The major version number. If this is greater than `0`, then two versions sharing
34 | the same major version number are not expected to be incompatible or have
35 | breaking changes.
36 |
37 |
38 | minor
39 |
40 | : number
41 |
42 |
43 |
44 | The minor version number. Describes version updates that are not enumerated by
45 | the major version number, such as versions prior to 1.0, or versions which
46 | are non-breaking.
47 |
48 |
49 | isRelease
50 |
51 | : boolean
52 |
53 |
54 |
55 | Describes whether the version was sourced from an official release package.
--------------------------------------------------------------------------------
/docs/api-reference/graph/members/observer.md:
--------------------------------------------------------------------------------
1 |
2 | Graph
3 | Members
4 | Observer
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.Observer(
17 | scope: Scope,
18 | watching: unknown
19 | ) -> Observer
20 | ```
21 |
22 | Constructs and returns a new [observer](../../types/observer).
23 |
24 | !!! success "Use scoped() method syntax"
25 | This function is intended to be accessed as a method on a scope:
26 | ```Lua
27 | local observer = scope:Observer(watching)
28 | ```
29 |
30 | -----
31 |
32 | ## Parameters
33 |
34 |
35 | scope
36 |
37 | : Scope <unknown>
38 |
39 |
40 |
41 | The [scope](../../../memory/types/scope) which should be used to store
42 | destruction tasks for this object.
43 |
44 |
45 | watching
46 |
47 | : unknown
48 |
49 |
50 |
51 | The target that the observer should watch for changes.
52 |
53 | !!! note "Works best with graph objects"
54 | While non-[graph object](../../types/graphobject) values are
55 | accepted for compatibility, they won't be able to trigger updates.
56 |
57 | -----
58 |
59 |
60 | Returns
61 |
62 | -> Observer
63 |
64 |
65 |
66 | A freshly constructed observer.
67 |
68 | -----
69 |
70 | ## Learn More
71 |
72 | - [Observers tutorial](../../../../tutorials/fundamentals/observers)
--------------------------------------------------------------------------------
/docs/api-reference/graph/types/observer.md:
--------------------------------------------------------------------------------
1 |
2 | Graph
3 | Types
4 | Observer
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type Observer = GraphObject & {
14 | type: "Observer",
15 | timeliness: "eager",
16 | onChange: (self, callback: () -> ()) -> (() -> ()),
17 | onBind: (self, callback: () -> ()) -> (() -> ())
18 | }
19 | ```
20 |
21 | A [graph object](../graphobject) that runs user code when it's updated by the
22 | reactive graph.
23 |
24 | !!! note "Non-standard type syntax"
25 | The above type definition uses `self` to denote methods. At time of writing,
26 | Luau does not interpret `self` specially.
27 |
28 | -----
29 |
30 | ## Members
31 |
32 |
33 | type
34 |
35 | : "Observer"
36 |
37 |
38 |
39 | A type string which can be used for runtime type checking.
40 |
41 | -----
42 |
43 | ## Methods
44 |
45 |
46 | onChange
47 |
48 | -> (() -> ())
49 |
50 |
51 |
52 | ```Lua
53 | function Observer:onChange(
54 | callback: () -> ()
55 | ): (() -> ())
56 | ```
57 |
58 | Registers the callback to run when an update is received.
59 |
60 | The returned function will unregister the callback.
61 |
62 |
63 | onBind
64 |
65 | -> (() -> ())
66 |
67 |
68 |
69 | ```Lua
70 | function Observer:onBind(
71 | callback: () -> ()
72 | ): (() -> ())
73 | ```
74 |
75 | Runs the callback immediately, and registers the callback to run when an update
76 | is received.
77 |
78 | The returned function will unregister the callback.
79 |
80 | -----
81 |
82 | ## Learn More
83 |
84 | - [Observers tutorial](../../../../tutorials/fundamentals/observers)
--------------------------------------------------------------------------------
/docs/api-reference/memory/members/derivescope.md:
--------------------------------------------------------------------------------
1 |
2 | Memory
3 | Members
4 | deriveScope
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.deriveScope(
17 | existing: Scope,
18 | ...: (AddMethods & {})...
19 | ): Scope
20 | ```
21 |
22 | Returns a blank [scope](../../types/scope) with the same methods as an existing
23 | scope, plus some optional additional methods which are merged in to only the
24 | new scope.
25 |
26 | Unlike [innerScope](../innerscope), the returned scope has a completely
27 | independent lifecycle from the original scope.
28 |
29 | !!! note "Pseudo type"
30 | Luau doesn't have adequate syntax to represent this function.
31 |
32 | !!! warning "Scopes are not unique"
33 | Fusion can recycle old unused scopes. This helps make scopes more
34 | lightweight, but it also means they don't uniquely belong to any part of
35 | your program.
36 |
37 | As a result, you shouldn't hold on to scopes after they've been cleaned up,
38 | and you shouldn't use them as unique identifiers anywhere.
39 |
40 | -----
41 |
42 | ## Parameters
43 |
44 |
45 | existing
46 |
47 | : Scope <T>
48 |
49 |
50 |
51 | An existing scope, whose methods should be re-used for the new scope.
52 |
53 |
54 | ...
55 |
56 | : AddMethods...
57 |
58 |
59 |
60 | A series of tables, ideally including functions which take a scope as their
61 | first parameter. Those functions will turn into methods on the scope.
62 |
63 | -----
64 |
65 |
66 | Returns
67 |
68 | -> Scope <T>
69 |
70 |
71 |
72 | A blank (non-inner) scope with the same methods as the existing scope, plus the
73 | extra methods provided.
74 |
75 | -----
76 |
77 | ## Learn More
78 |
79 | - [Scopes tutorial](../../../../tutorials/fundamentals/scopes)
--------------------------------------------------------------------------------
/docs/api-reference/memory/members/docleanup.md:
--------------------------------------------------------------------------------
1 |
2 | Memory
3 | Members
4 | doCleanup
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.doCleanup(
17 | task: Fusion.Task
18 | ): ()
19 | ```
20 |
21 | Attempts to destroy all arguments based on their runtime type.
22 |
23 | !!! warning "This is a black hole!"
24 | Any values you pass into `doCleanup` should be treated as completely gone.
25 | Make sure you remove all references to those values, and ensure your code
26 | never uses them again.
27 |
28 | -----
29 |
30 | ## Parameters
31 |
32 |
33 | task
34 |
35 | : Task
36 |
37 |
38 |
39 | A value which should be disposed of; the value's runtime type will be inspected
40 | to determine what should happen.
41 |
42 | - if `function`, it is called
43 | - ...else if `{destroy: (self) -> ()}`, `:destroy()` is called
44 | - ...else if `{Destroy: (self) -> ()}`, `:Destroy()` is called
45 | - ...else if `{any}`, `doCleanup` is called on all members
46 |
47 | When Fusion is running inside of Roblox:
48 |
49 | - if `Instance`, `:Destroy()` is called
50 | - ...else if `RBXScriptConnection`, `:Disconnect()` is called
51 |
52 | If none of these conditions match, the value is ignored.
53 |
54 | -----
55 |
56 | ## Learn More
57 |
58 | - [Scopes tutorial](../../../../tutorials/fundamentals/scopes)
--------------------------------------------------------------------------------
/docs/api-reference/memory/members/innerscope.md:
--------------------------------------------------------------------------------
1 |
2 | Memory
3 | Members
4 | innerScope
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.innerScope(
17 | existing: Scope,
18 | ...: (AddMethods & {})...
19 | ): Scope
20 | ```
21 |
22 | Returns a blank [scope](../../types/scope) with the same methods as an existing
23 | scope, plus some optional additional methods which are merged in to only the
24 | new scope.
25 |
26 | Unlike [deriveScope](../derivescope), the returned scope is an inner scope of
27 | the original scope. It exists until either the user calls `doCleanup` on it, or
28 | the original scope is cleaned up.
29 |
30 | !!! note "Pseudo type"
31 | Luau doesn't have adequate syntax to represent this function.
32 |
33 | !!! warning "Scopes are not unique"
34 | Fusion can recycle old unused scopes. This helps make scopes more
35 | lightweight, but it also means they don't uniquely belong to any part of
36 | your program.
37 |
38 | As a result, you shouldn't hold on to scopes after they've been cleaned up,
39 | and you shouldn't use them as unique identifiers anywhere.
40 |
41 | -----
42 |
43 | ## Parameters
44 |
45 |
46 | existing
47 |
48 | : Scope <T>
49 |
50 |
51 |
52 | An existing scope, whose methods should be re-used for the new scope.
53 |
54 |
55 | ...
56 |
57 | : AddMethods...
58 |
59 |
60 |
61 | A series of tables, ideally including functions which take a scope as their
62 | first parameter. Those functions will turn into methods on the scope.
63 |
64 | -----
65 |
66 |
67 | Returns
68 |
69 | -> Scope <T>
70 |
71 |
72 |
73 | A blank inner scope with the same methods as the existing scope, plus the
74 | extra methods provided.
75 |
76 | -----
77 |
78 | ## Learn More
79 |
80 | - [Scopes tutorial](../../../../tutorials/fundamentals/scopes)
--------------------------------------------------------------------------------
/docs/api-reference/memory/members/insert.md:
--------------------------------------------------------------------------------
1 |
2 | Memory
3 | Members
4 | insert
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.insert(
17 | scope: Scope,
18 | ...: Tasks...
19 | ): Tasks...
20 | ```
21 |
22 | Inserts destruction [tasks](../../types/task) passed in to the
23 | [scope](../../types/scope). Returns the clean up tasks to be used for variable
24 | declarations.
25 |
26 |
27 | !!! success "Use scoped() method syntax"
28 | This function is intended to be accessed as a method on a scope:
29 | ```Lua
30 | local conn, ins = scope:insert(
31 | RunService.Heartbeat:Connect(doUpdate),
32 | Instance.new("Part", workspace)
33 | )
34 | ```
35 |
36 | -----
37 |
38 | ## Parameters
39 |
40 |
41 | scope
42 |
43 | : Scope <unknown>
44 |
45 |
46 |
47 | The [scope](../../types/scope) which should be used to store
48 | destruction tasks.
49 |
50 |
51 | ...
52 |
53 | : Tasks...
54 |
55 |
56 |
57 | The destruction [tasks](../../types/task) which should be inserted into the
58 | scope.
59 |
60 | -----
61 |
62 |
63 | Returns
64 |
65 | -> Tasks...
66 |
67 |
68 |
69 | The destruction [tasks](../../types/task) that has been inserted into the scope.
70 |
71 | -----
72 |
73 | ## Learn More
74 |
75 | - [Scopes tutorial](../../../../tutorials/fundamentals/scopes)
76 |
--------------------------------------------------------------------------------
/docs/api-reference/memory/members/scoped.md:
--------------------------------------------------------------------------------
1 |
2 | Memory
3 | Members
4 | scoped
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.scoped(
17 | ...: (Methods & {})...
18 | ): Scope
19 | ```
20 |
21 | Returns a blank [scope](../../types/scope). Any method tables passed in as
22 | arguments are merged together, and used as the `__index` of the new scope, such
23 | that they can be called with method notation on the created scope.
24 |
25 | !!! note "Pseudo type"
26 | Luau doesn't have adequate syntax to represent this function.
27 |
28 | !!! warning "Scopes are not unique"
29 | Fusion can recycle old unused scopes. This helps make scopes more
30 | lightweight, but it also means they don't uniquely belong to any part of
31 | your program.
32 |
33 | As a result, you shouldn't hold on to scopes after they've been cleaned up,
34 | and you shouldn't use them as unique identifiers anywhere.
35 |
36 | -----
37 |
38 | ## Parameters
39 |
40 |
41 | ...
42 |
43 | : Methods & {}
44 |
45 |
46 |
47 | A series of tables, ideally including functions which take a scope as their
48 | first parameter. Those functions will turn into methods on the scope.
49 |
50 | -----
51 |
52 |
53 | Returns
54 |
55 | -> Scope <T>
56 |
57 |
58 |
59 | A blank scope with the specified methods.
60 |
61 | -----
62 |
63 | ## Learn More
64 |
65 | - [Scopes tutorial](../../../../tutorials/fundamentals/scopes)
--------------------------------------------------------------------------------
/docs/api-reference/memory/types/scope.md:
--------------------------------------------------------------------------------
1 |
2 | Memory
3 | Types
4 | Scope
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type Scope = {unknown} & Constructors
14 | ```
15 |
16 | A table collecting all objects created as part of an independent unit of code,
17 | with optional `Constructors` as methods which can be called.
18 |
19 | !!! warning "Scopes are not unique"
20 | Fusion can recycle old unused scopes. This helps make scopes more
21 | lightweight, but it also means they don't uniquely belong to any part of
22 | your program.
23 |
24 | As a result, you shouldn't hold on to scopes after they've been cleaned up,
25 | and you shouldn't use them as unique identifiers anywhere.
26 |
27 | -----
28 |
29 | ## Learn More
30 |
31 | - [Scopes tutorial](../../../../tutorials/fundamentals/scopes)
--------------------------------------------------------------------------------
/docs/api-reference/memory/types/scopedobject.md:
--------------------------------------------------------------------------------
1 |
2 | Memory
3 | Types
4 | ScopedObject
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type ScopedObject = {
14 | scope: Scope?,
15 | destroy: () -> ()
16 | }
17 | ```
18 |
19 | An object designed for use with [scopes](../../types/scope).
20 |
21 | Objects satisfying this interface can be probed for information about their
22 | lifetime and how long they live relative to other objects satisfying this
23 | interface.
24 |
25 | These objects are also recognised by [`doCleanup`](../../members/docleanup).
26 |
27 | -----
28 |
29 | ## Members
30 |
31 |
32 | scope
33 |
34 | : Scope <unknown>?
35 |
36 |
37 |
38 | The scope which this object was constructed with, or `nil` if the object has
39 | been destroyed.
40 |
41 | !!! note "Unchanged until destruction"
42 | The `scope` is expected to be set once upon construction. It should not be
43 | assigned to again, except when the scope is destroyed - at which point it
44 | should be set to `nil` to indicate that it no longer exists inside of a
45 | scope. This is typically done inside of `oldestTask`.
46 |
47 |
48 | oldestTask
49 |
50 | : unknown
51 |
52 |
53 |
54 | The value inside of `scope` representing the point at which the scoped object
55 | will be destroyed.
56 |
57 | !!! note "Unchanged until destruction"
58 | The `oldestTask` is expected to be set once upon construction. It should not
59 | be assigned to again.
60 |
61 | `oldestTask` is typically a callback that cleans up the object, but it's
62 | typed ambiguously here as it is only used as a reference for lifetime
63 | analysis, representing the point beyond which the object can be considered
64 | completely destroyed. It shouldn't be used for much else.
65 |
66 | -----
67 |
68 | ## Learn More
69 |
70 | - [Scopes tutorial](../../../../tutorials/fundamentals/scopes)
--------------------------------------------------------------------------------
/docs/api-reference/memory/types/task.md:
--------------------------------------------------------------------------------
1 |
2 | Memory
3 | Types
4 | Task
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type Task =
14 | Instance
15 | | RBXScriptConnection
16 | | () -> ()
17 | | {destroy: (self) -> ()}
18 | | {Destroy: (self) -> ()}
19 | | {Task}
20 | ```
21 | Types which [`doCleanup`](../../members/docleanup) has defined behaviour for.
22 |
23 | !!! warning "Not enforced"
24 | Fusion does not use static types to enforce that `doCleanup` is given a type
25 | which it can process.
26 |
27 | This type is only exposed for your own use.
28 |
29 | -----
30 |
31 | ## Learn More
32 |
33 | - [Scopes tutorial](../../../../tutorials/fundamentals/scopes)
--------------------------------------------------------------------------------
/docs/api-reference/roblox/members/attribute.md:
--------------------------------------------------------------------------------
1 |
2 | Roblox
3 | Members
4 | Attribute
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.Attribute(
17 | attributeName: string
18 | ): SpecialKey
19 | ```
20 |
21 | Given an attribute name, returns a [special key](../../types/specialkey) which
22 | can modify attributes of that name.
23 |
24 | When paired with a value in a [property table](../../types/propertytable), the
25 | special key sets the attribute to that value.
26 |
27 | -----
28 |
29 | ## Parameters
30 |
31 |
32 | attributeName
33 |
34 | : string
35 |
36 |
37 |
38 | The name of the attribute that the special key should target.
39 |
40 | -----
41 |
42 |
43 | Returns
44 |
45 | -> SpecialKey
46 |
47 |
48 |
49 | A special key for modifying attributes of that name.
--------------------------------------------------------------------------------
/docs/api-reference/roblox/members/attributechange.md:
--------------------------------------------------------------------------------
1 |
2 | Roblox
3 | Members
4 | AttributeChange
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.AttributeChange(
17 | attributeName: string
18 | ): SpecialKey
19 | ```
20 |
21 | Given an attribute name, returns a [special key](../../types/specialkey) which
22 | can listen to changes for attributes of that name.
23 |
24 | When paired with a callback in a [property table](../../types/propertytable),
25 | the special key connects the callback to the attribute's change event.
26 |
27 | -----
28 |
29 | ## Parameters
30 |
31 |
32 | attributeName
33 |
34 | : string
35 |
36 |
37 |
38 | The name of the attribute that the special key should target.
39 |
40 | -----
41 |
42 |
43 | Returns
44 |
45 | -> SpecialKey
46 |
47 |
48 |
49 | A special key for listening to changes for attributes of that name.
--------------------------------------------------------------------------------
/docs/api-reference/roblox/members/attributeout.md:
--------------------------------------------------------------------------------
1 |
2 | Roblox
3 | Members
4 | AttributeOut
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.AttributeOut(
17 | attributeName: string
18 | ): SpecialKey
19 | ```
20 |
21 | Given an attribute name, returns a [special key](../../types/specialkey) which
22 | can output values from attributes of that name.
23 |
24 | When paired with a [value object](../../../state/types/value) in a
25 | [property table](../../types/propertytable), the special key sets the value when
26 | the attribute changes.
27 |
28 | -----
29 |
30 | ## Parameters
31 |
32 |
33 | attributeName
34 |
35 | : string
36 |
37 |
38 |
39 | The name of the attribute that the special key should target.
40 |
41 | -----
42 |
43 |
44 | Returns
45 |
46 | -> SpecialKey
47 |
48 |
49 |
50 | A special key for outputting values from attributes of that name.
--------------------------------------------------------------------------------
/docs/api-reference/roblox/members/child.md:
--------------------------------------------------------------------------------
1 |
2 | Roblox
3 | Members
4 | Child
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.Child(
17 | child: Child
18 | ): Child
19 | ```
20 |
21 | Returns the [child](../../types/child) passed into it.
22 |
23 | This function does no processing. It only serves as a hint to the Luau type
24 | system, constraining the type of the argument.
25 |
26 | -----
27 |
28 | ## Parameters
29 |
30 |
31 | child
32 |
33 | : Child
34 |
35 |
36 |
37 | The argument whose type should be constrained.
38 |
39 | -----
40 |
41 |
42 | Returns
43 |
44 | -> Child
45 |
46 |
47 |
48 | The argument with the newly cast static type.
49 |
50 | -----
51 |
52 | ## Learn More
53 |
54 | - [Parenting tutorial](../../../../tutorials/roblox/parenting)
--------------------------------------------------------------------------------
/docs/api-reference/roblox/members/children.md:
--------------------------------------------------------------------------------
1 |
2 | Roblox
3 | Members
4 | Children
5 |
6 |
7 |
14 |
15 | ```Lua
16 | Fusion.Children: SpecialKey
17 | ```
18 |
19 | A [special key](../../types/specialkey) which parents other instances into this
20 | instance.
21 |
22 | When paired with a [`Child`](../../types/child) in a
23 | [property table](../../types/propertytable), the special key explores the
24 | `Child` to find every `Instance` nested inside. It then parents those instances
25 | under the instance which the special key was applied to.
26 |
27 | In particular, this special key will recursively explore arrays and bind to any
28 | [state objects](../../../state/types/stateobject).
29 |
30 | -----
31 |
32 | ## Learn More
33 |
34 | - [Parenting tutorial](../../../../tutorials/roblox/parenting)
--------------------------------------------------------------------------------
/docs/api-reference/roblox/members/hydrate.md:
--------------------------------------------------------------------------------
1 |
2 | Roblox
3 | Members
4 | Hydrate
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.Hydrate(
17 | target: Instance
18 | ): (
19 | props: PropertyTable
20 | ) -> Instance
21 | ```
22 |
23 | Given an instance, returns a component for binding extra functionality to that
24 | instance.
25 |
26 | In the property table, string keys are assigned as properties on the instance.
27 | If the value is a [state object](../../../state/types/stateobject), it is
28 | re-assigned every time the value of the state object changes.
29 |
30 | Any [special keys](../../types/specialkey) present in the property table are
31 | applied to the instance after string keys are processed, in the order specified
32 | by their `stage`.
33 |
34 | A special exception is made for assigning `Parent`, which is only assigned after
35 | the `descendants` stage.
36 |
37 | !!! warning "Do not overwrite properties"
38 | If the instance was previously created with [`New`](../new) or previously
39 | hydrated, do not assign to any properties that were previously specified in
40 | those prior calls. Duplicated assignments can interfere with each other in
41 | unpredictable ways.
42 |
43 | -----
44 |
45 | ## Parameters
46 |
47 |
48 | target
49 |
50 | : Instance
51 |
52 |
53 |
54 | The instance which should be modified.
55 |
56 | -----
57 |
58 |
59 | Returns
60 |
61 | -> (PropertyTable ) -> Instance
62 |
63 |
64 |
65 | A component that hydrates that instance, accepting various properties to build
66 | up bindings and operations applied to the instance.
67 |
68 | -----
69 |
70 | ## Learn More
71 |
72 | - [Hydration tutorial](../../../../tutorials/roblox/hydration)
--------------------------------------------------------------------------------
/docs/api-reference/roblox/members/new.md:
--------------------------------------------------------------------------------
1 |
2 | Roblox
3 | Members
4 | New
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.New(
17 | className: string
18 | ): (
19 | props: PropertyTable
20 | ) -> Instance
21 | ```
22 |
23 | Given a class name, returns a component for constructing instances of that
24 | class.
25 |
26 | In the property table, string keys are assigned as properties on the instance.
27 | If the value is a [state object](../../../state/types/stateobject), it is
28 | re-assigned every time the value of the state object changes.
29 |
30 | Any [special keys](../../types/specialkey) present in the property table are
31 | applied to the instance after string keys are processed, in the order specified
32 | by their `stage`.
33 |
34 | A special exception is made for assigning `Parent`, which is only assigned after
35 | the `descendants` stage.
36 |
37 | -----
38 |
39 | ## Parameters
40 |
41 |
42 | className
43 |
44 | : string
45 |
46 |
47 |
48 | The kind of instance that should be constructed.
49 |
50 | -----
51 |
52 |
53 | Returns
54 |
55 | -> (PropertyTable ) -> Instance
56 |
57 |
58 |
59 | A component that constructs instances of that type, accepting various properties
60 | to customise each instance uniquely.
61 |
62 | -----
63 |
64 | ## Learn More
65 |
66 | - [New Instances tutorial](../../../../tutorials/roblox/new-instances)
--------------------------------------------------------------------------------
/docs/api-reference/roblox/members/onchange.md:
--------------------------------------------------------------------------------
1 |
2 | Roblox
3 | Members
4 | OnChange
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.OnChange(
17 | propertyName: string
18 | ): SpecialKey
19 | ```
20 |
21 | Given an property name, returns a [special key](../../types/specialkey) which
22 | can listen to changes for properties of that name.
23 |
24 | When paired with a callback in a [property table](../../types/propertytable),
25 | the special key connects the callback to the property's change event.
26 |
27 | -----
28 |
29 | ## Parameters
30 |
31 |
32 | propertyName
33 |
34 | : string
35 |
36 |
37 |
38 | The name of the property that the special key should target.
39 |
40 | -----
41 |
42 |
43 | Returns
44 |
45 | -> SpecialKey
46 |
47 |
48 |
49 | A special key for listening to changes for properties of that name.
50 |
51 | -----
52 |
53 | ## Learn More
54 |
55 | - [Change Events tutorial](../../../../tutorials/roblox/change-events)
--------------------------------------------------------------------------------
/docs/api-reference/roblox/members/onevent.md:
--------------------------------------------------------------------------------
1 |
2 | Roblox
3 | Members
4 | OnEvent
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.OnEvent(
17 | eventName: string
18 | ): SpecialKey
19 | ```
20 |
21 | Given an event name, returns a [special key](../../types/specialkey) which
22 | can listen for events of that name.
23 |
24 | When paired with a callback in a [property table](../../types/propertytable),
25 | the special key connects the callback to the event.
26 |
27 | -----
28 |
29 | ## Parameters
30 |
31 |
32 | eventName
33 |
34 | : string
35 |
36 |
37 |
38 | The name of the event that the special key should target.
39 |
40 | -----
41 |
42 |
43 | Returns
44 |
45 | -> SpecialKey
46 |
47 |
48 |
49 | A special key for listening to events of that name.
50 |
51 | -----
52 |
53 | ## Learn More
54 |
55 | - [Events tutorial](../../../../tutorials/roblox/events)
--------------------------------------------------------------------------------
/docs/api-reference/roblox/members/out.md:
--------------------------------------------------------------------------------
1 |
2 | Roblox
3 | Members
4 | Out
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.Out(
17 | propertyName: string
18 | ): SpecialKey
19 | ```
20 |
21 | Given an property name, returns a [special key](../../types/specialkey) which
22 | can output values from properties of that name.
23 |
24 | When paired with a [value object](../../../state/types/value) in a
25 | [property table](../../types/propertytable), the special key sets the value when
26 | the property changes.
27 |
28 | -----
29 |
30 | ## Parameters
31 |
32 |
33 | propertyName
34 |
35 | : string
36 |
37 |
38 |
39 | The name of the property that the special key should target.
40 |
41 | -----
42 |
43 |
44 | Returns
45 |
46 | -> SpecialKey
47 |
48 |
49 |
50 | A special key for outputting values from properties of that name.
51 |
52 | -----
53 |
54 | ## Learn More
55 |
56 | - [Outputs tutorial](../../../../tutorials/roblox/outputs)
--------------------------------------------------------------------------------
/docs/api-reference/roblox/types/child.md:
--------------------------------------------------------------------------------
1 |
2 | Roblox
3 | Types
4 | Child
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type Child = Instance | StateObject | {[unknown]: Child}
14 | ```
15 |
16 | All of the types understood by the [`[Children]`](../../members/children)
17 | special key.
18 |
19 | -----
20 |
21 | ## Learn More
22 |
23 | - [Parenting tutorial](../../../../tutorials/roblox/parenting/)
24 | - [Instance Handling tutorial](../../../../tutorials/best-practices/instance-handling/)
--------------------------------------------------------------------------------
/docs/api-reference/roblox/types/propertytable.md:
--------------------------------------------------------------------------------
1 |
2 | Roblox
3 | Types
4 | PropertyTable
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type PropertyTable = {[string | SpecialKey]: unknown}
14 | ```
15 |
16 | A table of named instance properties and [special keys](../specialkey), which
17 | can be passed to [`New`](../../members/new) to create an instance.
18 |
19 | !!! warning "This type can be overly generic"
20 | In most cases, you should know what properties your code is looking for. In
21 | those cases, you should prefer to list out the properties explicitly, to
22 | document what your code needs.
23 |
24 | You should only use this type if you don't know what properties your code
25 | will accept.
26 |
--------------------------------------------------------------------------------
/docs/api-reference/state/members/computed.md:
--------------------------------------------------------------------------------
1 |
2 | State
3 | Members
4 | Computed
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.Computed(
17 | scope: Scope,
18 | processor: (Use, Scope) -> T
19 | ) -> Computed
20 | ```
21 |
22 | Constructs and returns a new [computed state object](../../types/computed).
23 |
24 | !!! success "Use scoped() method syntax"
25 | This function is intended to be accessed as a method on a scope:
26 | ```Lua
27 | local computed = scope:Computed(processor)
28 | ```
29 |
30 | -----
31 |
32 | ## Parameters
33 |
34 |
35 | scope
36 |
37 | : Scope <S>
38 |
39 |
40 |
41 | The [scope](../../../memory/types/scope) which should be used to store
42 | destruction tasks for this object.
43 |
44 |
45 | processor
46 |
47 | : (Use ,
48 | Scope <S>) -> T
49 |
50 |
51 |
52 | Computes the value that will be used by the computed. The processor is given a
53 | [use function](../../types/use) for including other objects in the
54 | computation, and a [scope](../../../memory/types/scope) for queueing destruction
55 | tasks to run on re-computation. The given scope has the same methods as the
56 | scope used to create the computed.
57 |
58 | -----
59 |
60 |
61 | Returns
62 |
63 | -> Computed <T>
64 |
65 |
66 |
67 | A freshly constructed computed state object.
68 |
69 | -----
70 |
71 | ## Learn More
72 |
73 | - [Computeds tutorial](../../../../tutorials/fundamentals/computeds)
--------------------------------------------------------------------------------
/docs/api-reference/state/members/peek.md:
--------------------------------------------------------------------------------
1 |
2 | State
3 | Members
4 | peek
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.peek(
17 | target: UsedAs
18 | ): T
19 | ```
20 |
21 | Extracts a value of type `T` from its input.
22 |
23 | This is a general-purpose implementation of [`Use`](../../types/use). It does
24 | not do any extra processing or book-keeping beyond what is required to determine
25 | the returned value.
26 |
27 | !!! warning "Specific implementations"
28 | If you're given a specific implementation of `Use` by an API, it's highly
29 | likely that you are expected to use that implementation instead of `peek()`.
30 |
31 | This applies to reusable code too. It's often best to ask for a `Use`
32 | callback if your code needs to extract values, so an appropriate
33 | implementation can be passed in.
34 |
35 | Alternatively for reusable code, you can avoid extracting values entirely,
36 | and expect the user to do it prior to calling your code. This can work well
37 | if you unconditionally use all inputs, but beware that you may end up
38 | extracting more values than you need - this can have performance
39 | implications.
40 |
41 | -----
42 |
43 | ## Parameters
44 |
45 |
46 | target
47 |
48 | : UsedAs <T>
49 |
50 |
51 |
52 | The abstract representation of `T` to extract a value from.
53 |
54 | -----
55 |
56 |
57 | Returns
58 |
59 | -> T
60 |
61 |
62 |
63 | The current value of `T`, derived from `target`.
64 |
65 | -----
66 |
67 | ## Learn More
68 |
69 | - [Values tutorial](../../../../tutorials/fundamentals/values/)
--------------------------------------------------------------------------------
/docs/api-reference/state/members/value.md:
--------------------------------------------------------------------------------
1 |
2 | State
3 | Members
4 | Value
5 |
6 |
7 |
14 |
15 | ```Lua
16 | function Fusion.Value(
17 | scope: Scope,
18 | initialValue: T
19 | ) -> Value
20 | ```
21 |
22 | Constructs and returns a new [value state object](../../types/value).
23 |
24 | !!! success "Use scoped() method syntax"
25 | This function is intended to be accessed as a method on a scope:
26 | ```Lua
27 | local computed = scope:Computed(processor)
28 | ```
29 |
30 | -----
31 |
32 | ## Parameters
33 |
34 |
35 | scope
36 |
37 | : Scope <S>
38 |
39 |
40 |
41 | The [scope](../../../memory/types/scope) which should be used to store
42 | destruction tasks for this object.
43 |
44 |
45 | initialValue
46 |
47 | : T
48 |
49 |
50 |
51 | The initial value that will be stored until the next value is `:set()`.
52 |
53 | -----
54 |
55 |
56 | Returns
57 |
58 | -> Value <T>
59 |
60 |
61 |
62 | A freshly constructed value state object.
63 |
64 | -----
65 |
66 | ## Learn More
67 |
68 | - [Values tutorial](../../../../tutorials/fundamentals/values)
--------------------------------------------------------------------------------
/docs/api-reference/state/types/computed.md:
--------------------------------------------------------------------------------
1 |
2 | State
3 | Types
4 | Computed
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type Computed = StateObject & {
14 | kind: "Computed",
15 | timeliness: "lazy"
16 | }
17 | ```
18 |
19 | A specialised [state object](../stateobject) for tracking single values computed
20 | from a user-defined computation.
21 |
22 | This type isn't generally useful outside of Fusion itself.
23 |
24 | -----
25 |
26 | ## Members
27 |
28 |
29 | kind
30 |
31 | : "Computed"
32 |
33 |
34 |
35 | A more specific type string which can be used for runtime type checking. This
36 | can be used to tell types of state object apart.
37 |
38 | -----
39 |
40 | ## Learn More
41 |
42 | - [Computeds tutorial](../../../../tutorials/fundamentals/computeds)
--------------------------------------------------------------------------------
/docs/api-reference/state/types/for.md:
--------------------------------------------------------------------------------
1 |
2 | State
3 | Types
4 | For
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type For = StateObject<{[KO]: VO}> & {
14 | kind: "For"
15 | }
16 | ```
17 |
18 | A specialised [state object](../stateobject) for tracking multiple values
19 | computed from user-defined computations, which are merged into an output table.
20 |
21 | This type isn't generally useful outside of Fusion itself.
22 |
23 | -----
24 |
25 | ## Members
26 |
27 |
28 | kind
29 |
30 | : "For"
31 |
32 |
33 |
34 | A more specific type string which can be used for runtime type checking. This
35 | can be used to tell types of state object apart.
36 |
37 | -----
38 |
39 | ## Learn More
40 |
41 | - [ForValues tutorial](../../../../tutorials/tables/forvalues)
42 | - [ForKeys tutorial](../../../../tutorials/tables/forkeys)
43 | - [ForPairs tutorial](../../../../tutorials/tables/forpairs)
--------------------------------------------------------------------------------
/docs/api-reference/state/types/stateobject.md:
--------------------------------------------------------------------------------
1 |
2 | State
3 | Types
4 | StateObject
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type StateObject = GraphObject & {
14 | type: "State",
15 | kind: string,
16 | _EXTREMELY_DANGEROUS_usedAsValue: T
17 | }
18 | ```
19 |
20 | Stores a value of `T` which can change over time. As a
21 | [graph object](../../../graph/types/graphobject), it can broadcast updates when its value changes.
22 |
23 | This type isn't generally useful outside of Fusion itself; you should prefer to
24 | work with [`UsedAs`](../usedas) in your own code.
25 |
26 | -----
27 |
28 | ## Members
29 |
30 |
31 | type
32 |
33 | : "State"
34 |
35 |
36 |
37 | A type string which can be used for runtime type checking.
38 |
39 |
40 | kind
41 |
42 | : string
43 |
44 |
45 |
46 | A more specific type string which can be used for runtime type checking. This
47 | can be used to tell types of state object apart.
48 |
49 |
50 | _EXTREMELY_DANGEROUS_usedAsValue
51 |
52 | : T
53 |
54 |
55 |
56 | !!! danger "This is for low-level library authors ***only!***"
57 | ***DO NOT USE THIS UNDER ANY CIRCUMSTANCES. IT IS UNNECESSARILY DANGEROUS TO
58 | DO SO.***
59 |
60 | You should ***never, ever*** access this in end user code. It doesn't
61 | matter if you think it'll save you from importing a function or typing a few
62 | characters. **YOUR CODE WILL NOT WORK.**
63 |
64 | If you choose to use it anyway, you give full permission for your employer
65 | to fire you immediately and personally defenestrate your laptop.
66 |
67 | The value that should be read out by any [use functions](../use). Implementors
68 | of the state object interface must ensure this property contains a valid value
69 | whenever the validity of the object is `valid`.
70 |
71 | This property **must never** invoke side effects in the reactive graph when
72 | read from or written to.
--------------------------------------------------------------------------------
/docs/api-reference/state/types/use.md:
--------------------------------------------------------------------------------
1 |
2 | State
3 | Types
4 | Use
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type Use = (target: UsedAs) -> T
14 | ```
15 |
16 | A function which extracts a value of `T` from something that can be
17 | [used as](../usedas) `T`.
18 |
19 | The most generic implementation of this is
20 | [the `peek()` function](../../members/peek), which performs this extraction with
21 | no additional steps.
22 |
23 | However, certain APIs may provide their own implementation,
24 | so they can perform additional processing for certain representations. Most
25 | notably, [computeds](../../members/computed) provide their own `use()` function
26 | which adds inputs to a watchlist, which allows them to re-calculate as inputs
27 | change.
28 |
29 | -----
30 |
31 | ## Parameters
32 |
33 |
34 | target
35 |
36 | : UsedAs <T>
37 |
38 |
39 |
40 | The representation of `T` to extract a value from.
41 |
42 | -----
43 |
44 |
45 | Returns
46 |
47 | -> T
48 |
49 |
50 |
51 | The current value of `T`, derived from `target`.
52 |
53 | -----
54 |
55 | ## Learn More
56 |
57 | - [Values tutorial](../../../../tutorials/fundamentals/values/)
58 | - [Computeds tutorial](../../../../tutorials/fundamentals/computeds/)
--------------------------------------------------------------------------------
/docs/api-reference/state/types/usedas.md:
--------------------------------------------------------------------------------
1 |
2 | State
3 | Types
4 | UsedAs
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type UsedAs = T | StateObject
14 | ```
15 |
16 | Something which describes a value of type `T`. When it is [used](../use) in a
17 | calculation, it becomes that value.
18 |
19 | !!! success "Recommended"
20 | Instead of using one of the more specific variants, your code should aim to
21 | use this type as often as possible. It allows your logic to deal with many
22 | representations of values at once,
23 |
24 | -----
25 |
26 | ## Variants
27 |
28 | - `T` - represents unchanging constant values
29 | - [`StateObject`](../stateobject) - represents dynamically updating values
30 |
31 | -----
32 |
33 | ## Learn More
34 |
35 | - [Components tutorial](../../../../tutorials/best-practices/components/)
--------------------------------------------------------------------------------
/docs/api-reference/state/types/value.md:
--------------------------------------------------------------------------------
1 |
2 | State
3 | Types
4 | Value
5 |
6 |
7 |
11 |
12 | ```Lua
13 | export type Value = StateObject & {
14 | kind: "State",
15 | set: (self, newValue: T) -> (),
16 | timeliness: "lazy"
17 | }
18 | ```
19 |
20 | A specialised [state object](../stateobject) which allows regular Luau code to
21 | control its value.
22 |
23 | !!! note "Non-standard type syntax"
24 | The above type definition uses `self` to denote methods. At time of writing,
25 | Luau does not interpret `self` specially.
26 |
27 | -----
28 |
29 | ## Members
30 |
31 |
32 | kind
33 |
34 | : "Value"
35 |
36 |
37 |
38 | A more specific type string which can be used for runtime type checking. This
39 | can be used to tell types of state object apart.
40 |
41 | -----
42 |
43 | ## Methods
44 |
45 |
46 | set
47 |
48 | -> T
49 |
50 |
51 |
52 | ```Lua
53 | function Value:set(
54 | newValue: T
55 | ): T
56 | ```
57 |
58 | Updates the value of this state object. Other objects using the value are
59 | notified of the change.
60 |
61 | The `newValue` is always returned, so that `:set()` can be used to capture
62 | values inside of expressions.
63 |
64 | -----
65 |
66 | ## Learn More
67 |
68 | - [Values tutorial](../../../../tutorials/fundamentals/values)
--------------------------------------------------------------------------------
/docs/assets/404-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/docs/assets/404-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/docs/assets/aura.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/assets/aura.png
--------------------------------------------------------------------------------
/docs/assets/heroes/api-reference.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/assets/heroes/api-reference.jpg
--------------------------------------------------------------------------------
/docs/assets/heroes/examples.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/assets/heroes/examples.jpg
--------------------------------------------------------------------------------
/docs/assets/home/fusion-clip-shape.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/assets/logo-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/assets/logo-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/assets/overrides/404.html:
--------------------------------------------------------------------------------
1 | {% extends "main.html" %}
2 |
3 |
4 | {% block content %}
5 |
6 |
7 |
8 | Oh noes!
9 |
10 |
11 | We couldn't find that page - it might have been moved or deleted.
12 |
13 |
14 |
15 | Here's some ideas on how to find it:
16 |
17 |
18 | Make sure there's no typos in the URL.
19 |
20 |
21 | Use the search box above to try and find the page.
22 |
23 |
24 | The page might not exist yet!
25 |
26 |
27 | Alternatively, return to the main page instead.
28 |
29 |
30 |
31 | {% endblock %}
32 |
33 |
34 | {% block disqus %}{% endblock %}
35 |
36 | {% block site_nav %}
37 |
38 | {% if nav %}
39 | {% if page.meta and page.meta.hide %}
40 | {% set hidden = "hidden" if "navigation" in page.meta.hide %}
41 | {% endif %}
42 |
54 | {% endif %}
55 | {% endblock %}
--------------------------------------------------------------------------------
/docs/assets/overrides/home.html:
--------------------------------------------------------------------------------
1 | {% extends "main.html" %}
2 |
3 | {% block content %}
4 |
5 | {{ super() }}
6 | {% endblock %}
--------------------------------------------------------------------------------
/docs/assets/overrides/main.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block fonts %}
4 |
5 | {% if config.theme.font != false %}
6 | {% set font = config.theme.font %}
7 |
8 |
14 |
20 |
26 | {% endif %}
27 | {% endblock %}
28 |
29 | {% block hero %}
30 | {% if page.meta and page.meta.hero_img %}
31 |
32 | {% endif %}
33 | {{ super() }}
34 | {% endblock %}
35 |
36 | {% block announce %}
37 | The information on this page is for a future version of Fusion. Use the version
38 | switcher to go back to a current version of Fusion.
39 | {% endblock %}
--------------------------------------------------------------------------------
/docs/assets/overrides/partials/content.html:
--------------------------------------------------------------------------------
1 |
2 | {% if page.edit_url and false %}
3 |
8 | {% include ".icons/octicons/pencil-24.svg" %}
9 |
10 | {% endif %}
11 |
12 |
17 | {% if not "\x3ch1" in page.content %}
18 | {{ page.title | d(config.site_name, true)}}
19 | {% endif %}
20 |
21 |
22 | {{ page.content }}
23 |
24 |
25 | {% if page and page.meta and (
26 | page.meta.git_revision_date_localized or
27 | page.meta.revision_date
28 | ) %}
29 | {% include "partials/source-file.html" %}
30 | {% endif %}
31 |
--------------------------------------------------------------------------------
/docs/assets/overrides/partials/footer.html:
--------------------------------------------------------------------------------
1 |
2 |
71 |
--------------------------------------------------------------------------------
/docs/assets/overrides/partials/logo.html:
--------------------------------------------------------------------------------
1 |
2 | {% if config.theme.logo %}
3 |
4 |
5 | {% else %}
6 | {% set icon = config.theme.icon.logo or "material/library" %}
7 | {% include ".icons/" ~ icon ~ ".svg" %}
8 | {% endif %}
9 |
--------------------------------------------------------------------------------
/docs/assets/overrides/partials/search.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/assets/scripts/error-paste-box.js:
--------------------------------------------------------------------------------
1 | try {
2 | function parseHeadings() {
3 | let parsed = [];
4 | for(element of document.querySelectorAll(".md-main .md-content .md-typeset h2")) {
5 | parsed.push({
6 | textLowercase: element.firstChild.textContent.toLowerCase(),
7 | element: element
8 | })
9 | }
10 | return parsed;
11 | }
12 | const headings = parseHeadings();
13 |
14 | function getMatchingHeadings(input) {
15 | const lowercaseInput = input.toLowerCase();
16 | let matches = [];
17 | for(heading of headings) {
18 | if(lowercaseInput.includes(heading.textLowercase)) {
19 | matches.push(heading);
20 | }
21 | }
22 | return matches;
23 | }
24 |
25 | const pasteBox = document.querySelector("#fusiondoc-error-paste-box");
26 |
27 | pasteBox.addEventListener("input", () => {
28 | let newText = pasteBox.value;
29 | let matches = getMatchingHeadings(newText);
30 |
31 | let match = matches.length == 1 ? matches[0] : null;
32 |
33 | for(heading of headings) {
34 | let container = heading.element.parentElement;
35 | if(match == null || match == heading) {
36 | container.classList.remove("fusiondoc-error-api-section-defocus");
37 | } else {
38 | container.classList.add("fusiondoc-error-api-section-defocus");
39 | }
40 | }
41 |
42 | if(match != null) {
43 | let heading = matches[0].element;
44 | let y = heading.getBoundingClientRect().top - 300;
45 | window.scrollTo({top: y, left: 0, behavior: "smooth"})
46 | }
47 | });
48 |
49 | } catch(e) {
50 | alert("Couldn't instantiate the error paste box - " + e);
51 | }
--------------------------------------------------------------------------------
/docs/assets/scripts/smooth-scroll.js:
--------------------------------------------------------------------------------
1 | document.body.addEventListener("click", e => {
2 | let link = null;
3 | let target = e.target;
4 |
5 | while(target != null) {
6 | if(target.tagName == "A") {
7 | link = target;
8 | break;
9 | } else {
10 | target = target.parentNode;
11 | }
12 | }
13 |
14 | if(link != null) {
15 | if(link.href.includes("#")) {
16 | // enable smooth scrolling when clicking on section links
17 | document.body.parentNode.classList.remove("fusiondoc-inhibit-smooth-scrolling");
18 | } else {
19 | // disable it everywhere else
20 | document.body.parentNode.classList.add("fusiondoc-inhibit-smooth-scrolling");
21 | }
22 | }
23 | })
--------------------------------------------------------------------------------
/docs/assets/theme/404.css:
--------------------------------------------------------------------------------
1 | #fusion-404 .graphic {
2 | display: block;
3 | width: 16rem;
4 | max-width: 80vmin;
5 | aspect-ratio: 1;
6 | margin: 0 auto;
7 | margin-bottom: 2rem;
8 | background-size: contain;
9 | background-position: center;
10 | background-repeat: no-repeat;
11 | }
12 |
13 | [data-md-color-scheme="fusiondoc-light"] #fusion-404 .graphic {
14 | background-image: url("../404-light.svg");
15 | }
16 |
17 | [data-md-color-scheme="fusiondoc-dark"] #fusion-404 .graphic {
18 | background-image: url("../404-dark.svg");
19 | }
20 |
21 | #fusion-404 h1 {
22 | text-align: center;
23 | font-size: 2rem;
24 | margin-top: 0;
25 | margin-bottom: 1rem;
26 | }
27 |
28 | #fusion-404 h2 {
29 | text-align: center;
30 | font-size: 1rem;
31 | font-weight: 400;
32 |
33 | margin-top: 0;
34 | margin-bottom: 2rem;
35 | }
36 |
37 | #fusion-404 .advice {
38 | max-width: max-content;
39 | margin: 0 auto;
40 | }
--------------------------------------------------------------------------------
/docs/assets/theme/admonition.css:
--------------------------------------------------------------------------------
1 | .md-typeset .admonition-title,
2 | .md-typeset summary {
3 | background: none !important;
4 | }
5 |
6 | .md-typeset :is(.admonition, details) {
7 | border-radius: 0.25rem;
8 | border-width: 0.1rem !important;
9 | box-shadow:
10 | 0 0.25em 1.5em -0.75em var(--admonition-color),
11 | inset 0 1em 2em -2.25em var(--admonition-color)
12 | !important;
13 |
14 | background: linear-gradient(var(--admonition-color) -99999%, transparent 5000%), var(--fusiondoc-bg-2);
15 | }
16 |
17 | .md-typeset :is(.admonition, details) > :is(.admonition-title, summary)::before {
18 | background-color: currentColor;
19 | }
20 |
21 | .md-typeset .note {
22 | --admonition-color: #448aff;
23 | }
24 | .md-typeset .abstract {
25 | --admonition-color: #00b0ff;
26 | }
27 | .md-typeset .info {
28 | --admonition-color: #00b8d4;
29 | }
30 | .md-typeset .tip {
31 | --admonition-color: #00bfa5;
32 | }
33 | .md-typeset .success {
34 | --admonition-color: #00c853;
35 | }
36 | .md-typeset .question {
37 | --admonition-color: #64dd17;
38 | }
39 | .md-typeset .warning {
40 | --admonition-color: #ff9100;
41 | }
42 | .md-typeset .failure {
43 | --admonition-color: #ff5252;
44 | }
45 | .md-typeset .danger {
46 | --admonition-color: #ff1744;
47 | }
48 | .md-typeset .bug {
49 | --admonition-color: #f50057;
50 | }
51 | .md-typeset .example {
52 | --admonition-color: #7c4dff;
53 | }
54 | .md-typeset .quote {
55 | --admonition-color: #9e9e9e;
56 | }
57 |
58 | [data-md-color-scheme="fusiondoc-dark"] .md-typeset .admonition.details {
59 | --md-code-bg-color: var(--fusiondoc-grey-1);
60 | }
--------------------------------------------------------------------------------
/docs/assets/theme/colours.css:
--------------------------------------------------------------------------------
1 | * {
2 | --md-default-fg-color: var(--fusiondoc-fg-1);
3 | --md-default-fg-color--light: var(--fusiondoc-fg-2);
4 | --md-default-fg-color--lighter: var(--fusiondoc-fg-3);
5 | --md-default-fg-color--lightest: var(--fusiondoc-fg-3);
6 | --md-default-bg-color: var(--fusiondoc-bg-1);
7 | --md-default-bg-color--light: var(--fusiondoc-bg-2);
8 | --md-default-bg-color--lighter: var(--fusiondoc-bg-3);
9 | --md-default-bg-color--lightest: var(--fusiondoc-bg-3);
10 |
11 | --md-primary-fg-color: var(--fusiondoc-accent);
12 | --md-primary-fg-color--light: var(--fusiondoc-accent-light);
13 | --md-primary-fg-color--dark: var(--fusiondoc-accent-dark);
14 |
15 | --md-accent-fg-color: var(--fusiondoc-accent-hover);
16 | --md-accent-fg-color--transparent: var(--fusiondoc-accent-hover-a10);
17 |
18 | --md-typeset-color: var(--md-default-fg-color);
19 | --md-typeset-a-color: var(--md-primary-fg-color);
20 | --md-typeset-mark-color: var(--fusiondoc-orange-alpha50);
21 | --md-typeset-del-color: hsla(6, 90%, 60%, 0.15);
22 | --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15);
23 | --md-typeset-kbd-color: hsla(0, 0%, 98%, 1);
24 | --md-typeset-kbd-accent-color: hsla(0, 100%, 100%, 1);
25 | --md-typeset-kbd-border-color: hsla(0, 0%, 72%, 1);
26 | --md-typeset-table-color: hsla(0, 0%, 0%, 0.12);
27 |
28 | --md-admonition-fg-color: var(--fusiondoc-fg-1);
29 | --md-admonition-bg-color: var(--fusiondoc-bg-2);
30 |
31 | --md-footer-fg-color: var(--fusiondoc-fg-2);
32 | --md-footer-fg-color--light: var(--fusiondoc-fg-2);
33 | --md-footer-fg-color--lighter: var(--fusiondoc-fg-2);
34 | --md-footer-bg-color: var(--fusiondoc-bg-1);
35 | --md-footer-bg-color--dark: var(--fusiondoc-bg-1);
36 | }
--------------------------------------------------------------------------------
/docs/assets/theme/dev-tools.css:
--------------------------------------------------------------------------------
1 | .fusiondoc-devtool-gallery {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
6 | .fusiondoc-devtool-entry {
7 | background-color: var(--fusiondoc-bg-2);
8 | box-shadow: var(--md-shadow-z1);
9 | border-radius: 0.25rem;
10 | padding: 0.75rem 1rem;
11 | margin: 0.5rem 0;
12 | margin-top: 0;
13 | }
14 |
15 | .fusiondoc-devtool-entry > h3 {
16 | margin-top: 0;
17 | color: var(--fusiondoc-fg-3);
18 | font-weight: 700;
19 | }
20 |
21 | .fusiondoc-devtool-entry > img {
22 | float: right;
23 | text-align: right;
24 | width: 5rem;
25 | aspect-ratio: 1;
26 | margin: 0;
27 | }
28 |
29 | .fusiondoc-devtool-entry > nav {
30 | display: flex;
31 | row-gap: 0.25rem;
32 | column-gap: 0.75rem;
33 | flex-wrap: wrap;
34 | }
35 |
36 | .fusiondoc-devtool-entry > nav > * {
37 | flex-shrink: 0;
38 | }
--------------------------------------------------------------------------------
/docs/assets/wip.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/assets/wordmark-tiny-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/assets/wordmark-tiny-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/docs/examples/cookbook/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Cookbook
3 | ---
4 |
5 | # Cookbook
6 |
7 | Oftentimes, you might be stuck on a small problem. You want to create something
8 | specific, but don't know how to do it with Fusion's tools.
9 |
10 | The cookbook can help with that! It's a collection of snippets which show you
11 | how to do various small tasks with Fusion, like processing arrays, applying
12 | animations and responding to different events.
13 |
14 | -----
15 |
16 | ## Navigation
17 |
18 | Using the sidebar to the left, you can browse all of the cookbook examples by
19 | name.
--------------------------------------------------------------------------------
/docs/examples/cookbook/light-and-dark-theme.md:
--------------------------------------------------------------------------------
1 | This example demonstrates how to create dynamic theme colours using Fusion's
2 | state objects.
3 |
4 | -----
5 |
6 | ## Overview
7 |
8 | ```Lua linenums="1"
9 | local Fusion = --initialise Fusion here however you please!
10 | local scoped = Fusion.scoped
11 | local peek = Fusion.peek
12 |
13 | local Theme = {}
14 |
15 | Theme.colours = {
16 | background = {
17 | light = Color3.fromHex("FFFFFF"),
18 | dark = Color3.fromHex("222222")
19 | },
20 | text = {
21 | light = Color3.fromHex("222222"),
22 | dark = Color3.fromHex("FFFFFF")
23 | }
24 | }
25 |
26 | -- Don't forget to pass this to `doCleanup` if you disable the script.
27 | local scope = scoped(Fusion)
28 |
29 | Theme.current = scope:Value("light")
30 | Theme.dynamic = {}
31 | for colour, variants in Theme.colours do
32 | Theme.dynamic[colour] = scope:Computed(function(use)
33 | return variants[use(Theme.current)]
34 | end)
35 | end
36 |
37 | Theme.current:set("light")
38 | print(peek(Theme.dynamic.background)) --> 255, 255, 255
39 |
40 | Theme.current:set("dark")
41 | print(peek(Theme.dynamic.background)) --> 34, 34, 34
42 | ```
43 |
44 | -----
45 |
46 | ## Explanation
47 |
48 | To begin, this example defines a set of colours with light and dark variants.
49 |
50 | ```Lua
51 | Theme.colours = {
52 | background = {
53 | light = Color3.fromHex("FFFFFF"),
54 | dark = Color3.fromHex("222222")
55 | },
56 | text = {
57 | light = Color3.fromHex("222222"),
58 | dark = Color3.fromHex("FFFFFF")
59 | }
60 | }
61 | ```
62 |
63 | A `Value` object stores which variant is in use right now.
64 |
65 | ```Lua
66 | Theme.current = scope:Value("light")
67 | ```
68 |
69 | Finally, each colour is turned into a `Computed`, which dynamically pulls the
70 | desired variant from the list.
71 |
72 | ```Lua
73 | Theme.dynamic = {}
74 | for colour, variants in Theme.colours do
75 | Theme.dynamic[colour] = scope:Computed(function(use)
76 | return variants[use(Theme.current)]
77 | end)
78 | end
79 | ```
80 |
81 | This allows other code to easily access theme colours from `Theme.dynamic`.
82 |
83 | ```Lua
84 | Theme.current:set("light")
85 | print(peek(Theme.dynamic.background)) --> 255, 255, 255
86 |
87 | Theme.current:set("dark")
88 | print(peek(Theme.dynamic.background)) --> 34, 34, 34
89 | ```
--------------------------------------------------------------------------------
/docs/examples/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | hero_img: ../heroes/examples.jpg
3 | ---
4 |
5 | # Examples
6 |
7 | Welcome to the Examples section! Here, you can find various open-source examples
8 | and projects, so you can see how Fusion works in a real setting.
9 |
10 | -----
11 |
12 | ## The Cookbook
13 |
14 | Oftentimes, you might be stuck on a small problem. You want to create something
15 | specific, but don't know how to do it with Fusion's tools.
16 |
17 | The cookbook can help with that! It's a collection of snippets which show you
18 | how to do various small tasks with Fusion, like processing arrays, applying
19 | animations and responding to different events.
20 |
21 | [Visit the cookbook to see what's available.](cookbook)
22 |
23 | -----
24 |
25 | ## Open-Source Projects
26 |
27 | ### Fusion Wordle (for Fusion 0.2)
28 |
29 | 
30 |
31 | See how Fusion can be used to build a mobile-first UI-centric game, with server
32 | validation, spring animations and sounds.
33 |
34 | [Play and edit the game on Roblox.](https://www.roblox.com/games/12178127791/)
35 |
36 | ### Fusion Obby (for Fusion 0.1)
37 |
38 | 
39 |
40 | See how Fusion can be used to build a minimal interface for an obby, with an
41 | animated checkpoint counter and simulated confetti.
42 |
43 | [Play and edit the game on Roblox.](https://www.roblox.com/games/7262692194/Fusion-Obby)
--------------------------------------------------------------------------------
/docs/examples/place-thumbnails/Fusion-Obby.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/examples/place-thumbnails/Fusion-Obby.jpg
--------------------------------------------------------------------------------
/docs/examples/place-thumbnails/Fusion-Wordle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/examples/place-thumbnails/Fusion-Wordle.jpg
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Damping-Critical-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Damping-Critical-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Damping-Critical-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Damping-Critical-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Damping-Over-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Damping-Over-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Damping-Over-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Damping-Over-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Damping-Under-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Damping-Under-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Damping-Under-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Damping-Under-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Damping-Zero-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Damping-Zero-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Damping-Zero-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Damping-Zero-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Following-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Following-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Following-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Following-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Interrupted-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Interrupted-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Interrupted-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Interrupted-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Speed-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Speed-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Speed-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Speed-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Step-Basic-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Step-Basic-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/springs/Step-Basic-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/springs/Step-Basic-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Delay-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Delay-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Delay-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Delay-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Easing-Direction-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Easing-Direction-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Easing-Direction-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Easing-Direction-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Easing-Style-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Easing-Style-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Easing-Style-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Easing-Style-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Follow-Failure-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Follow-Failure-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Follow-Failure-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Follow-Failure-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Interrupted-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Interrupted-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Interrupted-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Interrupted-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Repeats-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Repeats-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Repeats-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Repeats-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Reversing-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Reversing-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Reversing-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Reversing-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Step-Basic-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Step-Basic-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Step-Basic-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Step-Basic-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Time-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Time-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/animation/tweens/Time-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/animation/tweens/Time-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/fundamentals/values.md:
--------------------------------------------------------------------------------
1 | Now that you understand how Fusion works with objects, you can create Fusion's
2 | simplest object.
3 |
4 | Values are objects which store single values. You can write to them with
5 | their `:set()` method, and read from them with the `peek()` function.
6 |
7 | ```Lua
8 | local health = scope:Value(100)
9 |
10 | print(peek(health)) --> 100
11 | health:set(25)
12 | print(peek(health)) --> 25
13 | ```
14 |
15 | -----
16 |
17 | ## Usage
18 |
19 | To create a new value object, call `scope:Value()` and give it a value you want
20 | to store.
21 |
22 | ```Lua linenums="2" hl_lines="5"
23 | local Fusion = require(ReplicatedStorage.Fusion)
24 | local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped
25 |
26 | local scope = scoped(Fusion)
27 | local health = scope:Value(5)
28 | ```
29 |
30 | Fusion provides a global `peek()` function. It will read the value of whatever
31 | you give it. You'll use `peek()` to read the value of lots of things; for now,
32 | it's useful for printing `health` back out.
33 |
34 | ```Lua linenums="2" hl_lines="3 7"
35 | local Fusion = require(ReplicatedStorage.Fusion)
36 | local doCleanup, scoped = Fusion.doCleanup, Fusion.scoped
37 | local peek = Fusion.peek
38 |
39 | local scope = scoped(Fusion)
40 | local health = scope:Value(5)
41 | print(peek(health)) --> 5
42 | ```
43 |
44 | You can change the value using the `:set()` method. Unlike `peek()`, this is
45 | specific to value objects, so it's done on the object itself.
46 |
47 | ```Lua linenums="6" hl_lines="5-6"
48 | local scope = scoped(Fusion)
49 | local health = scope:Value(5)
50 | print(peek(health)) --> 5
51 |
52 | health:set(25)
53 | print(peek(health)) --> 25
54 | ```
55 |
56 | ??? tip "`:set()` returns the value you give it"
57 | You can use `:set()` in the middle of calculations:
58 |
59 | ```Lua
60 | local myNumber = scope:Value(0)
61 | local computation = 10 + myNumber:set(2 + 2)
62 | print(computation) --> 14
63 | print(peek(myNumber)) --> 4
64 | ```
65 |
66 | This is useful when building complex expressions. On a later page, you'll
67 | see one such use case.
68 |
69 | Generally though, it's better to keep your expressions simple.
70 |
71 | Value objects are Fusion's simplest 'state object'. State objects contain a
72 | single value - their *state*, you might say - and that single value can be read
73 | out at any time using `peek()`.
74 |
75 | Later on, you'll discover more advanced state objects that can calculate their
76 | value in more interesting ways.
--------------------------------------------------------------------------------
/docs/tutorials/get-started/developer-tools/community/codify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/get-started/developer-tools/community/codify.png
--------------------------------------------------------------------------------
/docs/tutorials/get-started/developer-tools/community/flipbook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/get-started/developer-tools/community/flipbook.png
--------------------------------------------------------------------------------
/docs/tutorials/get-started/developer-tools/community/hoarcekat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/get-started/developer-tools/community/hoarcekat.png
--------------------------------------------------------------------------------
/docs/tutorials/get-started/developer-tools/community/lydie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/get-started/developer-tools/community/lydie.png
--------------------------------------------------------------------------------
/docs/tutorials/get-started/developer-tools/community/onyxui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/get-started/developer-tools/community/onyxui.png
--------------------------------------------------------------------------------
/docs/tutorials/get-started/developer-tools/community/rojo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/get-started/developer-tools/community/rojo.png
--------------------------------------------------------------------------------
/docs/tutorials/get-started/getting-help.md:
--------------------------------------------------------------------------------
1 | If you're struggling to understand a concept, or need help debugging an error,
2 | here are some resources which can help.
3 |
4 | -----
5 |
6 | ## Get Help With Errors
7 |
8 | Whenever Fusion outputs a message to the console, it will contain a link to a
9 | page which will tell you more about what the message means, and why it appeared.
10 |
11 | ```hl_lines="3"
12 | [Fusion] The Frame class doesn't have a property called 'Activated'.
13 | ID: cannotConnectChange
14 | Learn more: https://elttob.uk/Fusion/0.3/api-reference/general/errors/#cannotconnectchange
15 | ```
16 |
17 | When you follow that link, it will take you to
18 | [the Errors page](../../../api-reference/general/errors), which describes every
19 | single message that Fusion can show you, what parts of Fusion are related to
20 | each message, and any relevant ongoing discussions on the Fusion repository that
21 | may contain useful context.
22 |
23 | When you run into an error, that page is a great place to start!
24 |
25 | -----
26 |
27 | ## Working Examples
28 |
29 | If you would like to see more practical examples of Fusion being used to build
30 | larger systems, then take a look at
31 | [the Examples section](../../../examples).
32 |
33 | The example projects can be a great place to learn how Fusion code should look
34 | in a complete project, and help you to structure your own projects in ways that
35 | are easy to extend as you grow.
36 |
37 | Additionally, there's a [cookbook](../../../examples/cookbook) full of explained
38 | code snippets, which show you how to achieve common tasks in an idiomatic and
39 | professional way using Fusion.
40 |
41 | -----
42 |
43 | ## Talk To Other Developers
44 |
45 | Fusion is built to be easy to use, and this website strives to be as useful and
46 | comprehensive as possible. However, you might need targeted help on a specific
47 | issue, or you might want to grow your understanding of Fusion in other ways.
48 |
49 | The best place to get help is [the #fusion channel](https://discord.com/channels/385151591524597761/895437663040077834)
50 | over on [the Roblox OSS Discord server](https://discord.gg/h2NV8PqhAD).
51 | Maintainers and contributors drop in frequently, alongside many eager Fusion
52 | users.
--------------------------------------------------------------------------------
/docs/tutorials/get-started/installing-fusion/Github-Releases-Guide-1-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/get-started/installing-fusion/Github-Releases-Guide-1-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/get-started/installing-fusion/Github-Releases-Guide-1-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/get-started/installing-fusion/Github-Releases-Guide-1-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/get-started/installing-fusion/Github-Releases-Guide-2-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/get-started/installing-fusion/Github-Releases-Guide-2-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/get-started/installing-fusion/Github-Releases-Guide-2-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/get-started/installing-fusion/Github-Releases-Guide-2-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/get-started/installing-fusion/Github-Releases-Guide-3-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/get-started/installing-fusion/Github-Releases-Guide-3-Dark.png
--------------------------------------------------------------------------------
/docs/tutorials/get-started/installing-fusion/Github-Releases-Guide-3-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dphfox/Fusion/a2a4e6b2d9a9af5f5d0416b1b619798d5e4c0564/docs/tutorials/get-started/installing-fusion/Github-Releases-Guide-3-Light.png
--------------------------------------------------------------------------------
/docs/tutorials/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | hero_img: ../home/Hero-Light.svg
3 | ---
4 |
5 | Welcome to the Fusion tutorial section! Here, you'll learn how to build great
6 | things with Fusion, even if you're a complete newcomer to the library.
7 |
8 | You'll not only learn how Fusion's features work, but you'll also be presented
9 | with wisdom from those who've worked with some of the largest Fusion codebases
10 | today.
11 |
12 | !!! warning "But first, some advice from the maintainers..."
13 | **
14 | Fusion is pre-1.0 software.
15 | **
16 |
17 | We *(the maintainers and contributors)* work hard to keep releases bug-free
18 | and relatively complete, so it should be safe to use in production. Many
19 | people already do, and report fantastic results!
20 |
21 | However, we mark Fusion as pre-1.0 because we are working on the design of
22 | the library itself. We strive for the best library design we can deliver,
23 | which means breaking changes are common and sweeping.
24 |
25 | With Fusion, you should expect:
26 |
27 | - upgrades to be frictionful, requiring code to be rethought
28 | - features to be superseded or removed across versions
29 | - advice or best practices to change over time
30 |
31 | You should *also* expect:
32 |
33 | - careful consideration around breakage, even though we reserve the right to
34 | do it
35 | - clear communication ahead of any major changes
36 | - helpful advice to answer your questions and ease your porting process
37 |
38 | We hope you enjoy using Fusion!
39 |
40 | -----
41 |
42 | ## What You Need To Know
43 |
44 | These tutorials assume:
45 |
46 | - That you're comfortable with the Luau scripting language.
47 | - These tutorials aren't an introduction to Luau! If you'd like to learn,
48 | check out the [Roblox documentation](https://create.roblox.com/docs).
49 | - That - if you're using Roblox features - you're familiar with how Roblox works.
50 | - You don't have to be an expert! Knowing about basic instances, events
51 | and data types will be good enough.
52 |
53 | Based on your existing knowledge, you may find some tutorials easier or harder.
54 | Don't be discouraged - Fusion's built to be easy to learn, but it may still take
55 | a bit of time to absorb some concepts. Learn at a pace which is right for you.
--------------------------------------------------------------------------------
/docs/tutorials/roblox/change-events.md:
--------------------------------------------------------------------------------
1 | `OnChange` is a function that returns keys to use when hydrating or creating an
2 | instance. Those keys let you connect functions to property changed events on the
3 | instance.
4 |
5 | ```Lua
6 | local input = scope:New "TextBox" {
7 | [OnChange "Text"] = function(newText)
8 | print("You typed:", newText)
9 | end
10 | }
11 | ```
12 |
13 | -----
14 |
15 | ## Usage
16 |
17 | `OnChange` doesn't need a scope - import it into your code from Fusion directly.
18 |
19 | ```Lua
20 | local OnChange = Fusion.OnChange
21 | ```
22 |
23 | When you call `OnChange` with a property name, it will return a special key:
24 |
25 | ```Lua
26 | local key = OnChange("Text")
27 | ```
28 |
29 | When used in a property table, you can pass in a handler and it will be run when
30 | that property changes.
31 |
32 | !!! info "Arguments are different to Roblox API"
33 | Normally in the Roblox API, when using `:GetPropertyChangedSignal()` on an
34 | instance, the callback will not receive any arguments.
35 |
36 | To make working with change events easier, `OnChange` will pass the new value of
37 | the property to the callback.
38 |
39 | ```Lua
40 | local input = scope:New "TextBox" {
41 | [OnChange("Text")] = function(newText)
42 | print("You typed:", newText)
43 | end
44 | }
45 | ```
46 |
47 | If you're using quotes `'' ""` for the event name, the extra parentheses `()`
48 | are optional:
49 |
50 | ```Lua
51 | local input = scope:New "TextBox" {
52 | [OnChange "Text"] = function(newText)
53 | print("You typed:", newText)
54 | end
55 | }
56 | ```
57 |
58 |
--------------------------------------------------------------------------------
/docs/tutorials/roblox/events.md:
--------------------------------------------------------------------------------
1 | `OnEvent` is a function that returns keys to use when hydrating or creating an
2 | instance. Those keys let you connect functions to events on the instance.
3 |
4 | ```Lua
5 | local button = scope:New "TextButton" {
6 | [OnEvent "Activated"] = function(_, numClicks)
7 | print("The button was pressed", numClicks, "time(s)!")
8 | end
9 | }
10 | ```
11 |
12 | -----
13 |
14 | ## Usage
15 |
16 | `OnEvent` doesn't need a scope - import it into your code from Fusion directly.
17 |
18 | ```Lua
19 | local OnEvent = Fusion.OnEvent
20 | ```
21 |
22 | When you call `OnEvent` with an event name, it will return a special key:
23 |
24 | ```Lua
25 | local key = OnEvent("Activated")
26 | ```
27 |
28 | When that key is used in a property table, you can pass in a handler and it will
29 | be connected to the event for you:
30 |
31 | ```Lua
32 | local button = scope:New "TextButton" {
33 | [OnEvent("Activated")] = function(_, numClicks)
34 | print("The button was pressed", numClicks, "time(s)!")
35 | end
36 | }
37 | ```
38 |
39 | If you're using quotes `'' ""` for the event name, the extra parentheses `()`
40 | are optional:
41 |
42 | ```Lua
43 | local button = scope:New "TextButton" {
44 | [OnEvent "Activated"] = function(_, numClicks)
45 | print("The button was pressed", numClicks, "time(s)!")
46 | end
47 | }
48 | ```
--------------------------------------------------------------------------------
/docs/tutorials/roblox/new-instances.md:
--------------------------------------------------------------------------------
1 | Fusion provides a `New` function when you're hydrating newly-made instances. It
2 | creates a new instance, applies some default properties, then hydrates it with
3 | a property table.
4 |
5 | ```Lua
6 | local message = scope:Value("Hello there!")
7 |
8 | local ui = scope:New "TextLabel" {
9 | Name = "Greeting",
10 | Parent = PlayerGui.ScreenGui,
11 |
12 | Text = message
13 | }
14 |
15 | print(ui.Name) --> Greeting
16 | print(ui.Text) --> Hello there!
17 |
18 | message:set("Goodbye friend!")
19 | task.wait() -- important: changes are applied on the next frame!
20 | print(ui.Text) --> Goodbye friend!
21 | ```
22 |
23 | -----
24 |
25 | ## Usage
26 |
27 | The `New` function is called in two parts. First, call the function with the
28 | type of instance, then pass in the property table:
29 |
30 | ```Lua
31 | local instance = scope:New("Part")({
32 | Parent = workspace,
33 | Color = Color3.new(1, 0, 0)
34 | })
35 | ```
36 |
37 | If you're using curly braces `{}` for your properties, and quotes `'' ""` for
38 | your class type, the extra parentheses `()` are optional:
39 |
40 | ```Lua
41 | -- This only works when you're using curly braces {} and quotes '' ""!
42 | local instance = scope:New "Part" {
43 | Parent = workspace,
44 | Color = Color3.new(1, 0, 0)
45 | }
46 | ```
47 |
48 | By design, `New` works just like `Hydrate` - it will apply properties the same
49 | way. [See the Hydrate tutorial to learn more.](../hydration)
50 |
51 | -----
52 |
53 | ## Default Properties
54 |
55 | When you create an instance using `Instance.new()`, Roblox will give it some
56 | default properties. However, these tend to be outdated and aren't useful for
57 | most people, leading to repetitive boilerplate needed to disable features that
58 | nobody wants to use.
59 |
60 | The `New` function will apply some of it's own default properties to fix this.
61 | For example, by default borders on UI are disabled, automatic colouring is
62 | turned off and default content is removed.
63 |
64 | 
65 | 
66 |
67 | For a complete list, [take a look at Fusion's default properties file.](https://github.com/Elttob/Fusion/blob/main/src/Instances/defaultProps.luau)
68 |
--------------------------------------------------------------------------------
/gh-assets/clearfloat.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@dphfox/fusion",
3 | "version": "0.4.0",
4 | "license": "MIT",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/dphfox/Fusion.git"
8 | },
9 | "contributors": [
10 | "dphfox"
11 | ],
12 | "bugs": {
13 | "url": "https://github.com/dphfox/Fusion/issues"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/selene.toml:
--------------------------------------------------------------------------------
1 | std = "roblox"
2 | exclude = ["test/TestEZ/**"]
3 |
4 | [rules]
5 | # Too aggressive in tests
6 | unused_variable = "allow"
7 | # Handled by formatting/StyLua
8 | multiple_statements = "allow"
9 | # Used for Luau type assertions
10 | shadowing = "allow"
11 |
--------------------------------------------------------------------------------
/src/Animation/ExternalTime.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Outputs the current external time as a state object.
8 | ]]
9 |
10 | local Package = script.Parent.Parent
11 | local Types = require(Package.Types)
12 | local External = require(Package.External)
13 | -- Graph
14 | local change = require(Package.Graph.change)
15 | -- Utility
16 | local nicknames = require(Package.Utility.nicknames)
17 |
18 | type ExternalTime = Types.StateObject
19 |
20 | type Self = ExternalTime
21 |
22 | local class = {}
23 | class.type = "State"
24 | class.kind = "ExternalTime"
25 | class.timeliness = "lazy"
26 | class.dependencySet = table.freeze {}
27 | class._EXTREMELY_DANGEROUS_usedAsValue = External.lastUpdateStep()
28 |
29 | local METATABLE = table.freeze {__index = class}
30 |
31 | local allTimers: {Self} = {}
32 |
33 | local function ExternalTime(
34 | scope: Types.Scope
35 | ): ExternalTime
36 | local createdAt = os.clock()
37 | local self: Self = setmetatable(
38 | {
39 | createdAt = createdAt,
40 | dependentSet = {},
41 | lastChange = nil,
42 | scope = scope,
43 | validity = "invalid"
44 | },
45 | METATABLE
46 | ) :: any
47 | local destroy = function()
48 | self.scope = nil
49 | local index = table.find(allTimers, self)
50 | if index ~= nil then
51 | table.remove(allTimers, index)
52 | end
53 | end
54 | self.oldestTask = destroy
55 | nicknames[self.oldestTask] = "ExternalTime"
56 | table.insert(scope, destroy)
57 | table.insert(allTimers, self)
58 | return self
59 | end
60 |
61 | function class._evaluate(
62 | self: Self
63 | ): boolean
64 | -- While someone else could call `change()` on this object, it wouldn't be
65 | -- idiomatic. So, since the only idiomatic time this function runs is when
66 | -- the external update step runs, it's safe enough to assume that the result
67 | -- has always meaningfully changed. The worst that can happen is unexpected
68 | -- refreshing for people doing unorthodox shenanigans, which is an OK trade.
69 | return true
70 | end
71 |
72 | External.bindToUpdateStep(function(
73 | externalNow: number
74 | ): ()
75 | class._EXTREMELY_DANGEROUS_usedAsValue = External.lastUpdateStep()
76 | for _, timer in allTimers do
77 | change(timer)
78 | end
79 | end)
80 |
81 | -- Do *not* freeze the class table, because it stores the shared value of all
82 | -- external time objects, and is updated every frame because of that.
83 | -- table.freeze(class)
84 | return ExternalTime
--------------------------------------------------------------------------------
/src/Animation/getTweenDuration.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Given a `tweenInfo`, returns how many seconds it will take before the tween
8 | finishes moving. The result may be infinite if the tween repeats forever.
9 | ]]
10 |
11 | local TweenService = game:GetService("TweenService")
12 |
13 | local function getTweenDuration(
14 | tweenInfo: TweenInfo
15 | ): number
16 | if tweenInfo.RepeatCount <= -1 then
17 | return math.huge
18 | end
19 | local tweenDuration = tweenInfo.DelayTime + tweenInfo.Time
20 | if tweenInfo.Reverses then
21 | tweenDuration += tweenInfo.Time
22 | end
23 | tweenDuration *= tweenInfo.RepeatCount + 1
24 | return tweenDuration
25 | end
26 |
27 | return getTweenDuration
28 |
--------------------------------------------------------------------------------
/src/Animation/getTweenRatio.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Given a `tweenInfo` and `currentTime`, returns a ratio which can be used to
8 | tween between two values over time.
9 | ]]
10 |
11 | local TweenService = game:GetService("TweenService")
12 |
13 | local function getTweenRatio(
14 | tweenInfo: TweenInfo,
15 | currentTime: number
16 | ): number
17 | local delay = tweenInfo.DelayTime
18 | local duration = tweenInfo.Time
19 | local reverses = tweenInfo.Reverses
20 | local numCycles = 1 + tweenInfo.RepeatCount
21 | local easeStyle = tweenInfo.EasingStyle
22 | local easeDirection = tweenInfo.EasingDirection
23 | local cycleDuration = delay + duration
24 | if reverses then
25 | cycleDuration += duration
26 | end
27 | -- If currentTime is infinity, then presumably the tween should be over.
28 | -- This avoids NaN when the duration of an infinitely repeating tween is given.
29 | if currentTime == math.huge then
30 | return 1
31 | end
32 | if currentTime >= cycleDuration * numCycles and tweenInfo.RepeatCount > -1 then
33 | return 1
34 | end
35 | local cycleTime = currentTime % cycleDuration
36 | if cycleTime <= delay then
37 | return 0
38 | end
39 | local tweenProgress = (cycleTime - delay) / duration
40 | if tweenProgress > 1 then
41 | tweenProgress = 2 - tweenProgress
42 | end
43 | local ratio = TweenService:GetValue(tweenProgress, easeStyle, easeDirection)
44 | return ratio
45 | end
46 |
47 | return getTweenRatio
48 |
--------------------------------------------------------------------------------
/src/Colour/sRGB.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Provides transformation functions for converting linear RGB values
8 | into sRGB values.
9 |
10 | RGB color channel transformations are outlined here:
11 | https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F
12 | ]]
13 |
14 | local sRGB = {}
15 |
16 | -- Equivalent to f_inv. Takes a linear sRGB channel and returns
17 | -- the sRGB channel
18 | local function transform(channel: number): number
19 | if channel >= 0.04045 then
20 | return ((channel + 0.055)/(1 + 0.055))^2.4
21 | else
22 | return channel / 12.92
23 | end
24 | end
25 |
26 | -- Equivalent to f. Takes an sRGB channel and returns
27 | -- the linear sRGB channel
28 | local function inverse(channel: number): number
29 | if channel >= 0.0031308 then
30 | return (1.055) * channel^(1.0/2.4) - 0.055
31 | else
32 | return 12.92 * channel
33 | end
34 | end
35 |
36 | -- Uses a transformation to convert linear RGB into sRGB.
37 | function sRGB.fromLinear(rgb: Color3): Color3
38 | return Color3.new(
39 | transform(rgb.R),
40 | transform(rgb.G),
41 | transform(rgb.B)
42 | )
43 | end
44 |
45 | -- Converts an sRGB into linear RGB using a
46 | -- (The inverse of sRGB.fromLinear).
47 | function sRGB.toLinear(srgb: Color3): Color3
48 | return Color3.new(
49 | inverse(srgb.R),
50 | inverse(srgb.G),
51 | inverse(srgb.B)
52 | )
53 | end
54 |
55 | return sRGB
--------------------------------------------------------------------------------
/src/ExternalDebug.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Abstraction layer between Fusion internals and external debuggers, allowing
8 | for deep introspection using function hooks.
9 |
10 | Unlike `External`, attaching a debugger is optional, and all debugger
11 | functions are expected to be infallible and non-blocking.
12 | ]]
13 |
14 | local Package = script.Parent
15 | local Types = require(Package.Types)
16 |
17 | local currentProvider: Types.ExternalDebugger? = nil
18 | local lastUpdateStep = 0
19 |
20 | local Debugger = {}
21 |
22 | --[[
23 | Swaps to a new debugger.
24 | Returns the old debugger, so it can be used again later.
25 | ]]
26 | function Debugger.setDebugger(
27 | newProvider: Types.ExternalDebugger?
28 | ): Types.ExternalDebugger?
29 | local oldProvider = currentProvider
30 | if oldProvider ~= nil then
31 | oldProvider.stopDebugging()
32 | end
33 | currentProvider = newProvider
34 | if newProvider ~= nil then
35 | newProvider.startDebugging()
36 | end
37 | return oldProvider
38 | end
39 |
40 | --[[
41 | Called at the earliest moment after a scope is created or removed from the
42 | scope pool, but not before the scope has finished being prepared by the
43 | library, so that debuggers can register its existence and track changes
44 | to the scope over time.
45 | ]]
46 | function Debugger.trackScope(
47 | scope: Types.Scope
48 | ): ()
49 | if currentProvider == nil then
50 | return
51 | end
52 | currentProvider.trackScope(scope)
53 | end
54 |
55 | --[[
56 | Called at the final moment before a scope is poisoned or added to the scope
57 | pool, after all cleanup tasks have completed, so that debuggers can erase
58 | the scope from internal trackers. Note that, due to scope pooling and user
59 | code, never assume that this correlates with garbage collection events.
60 | ]]
61 | function Debugger.untrackScope(
62 | scope: Types.Scope
63 | ): ()
64 | if currentProvider == nil then
65 | return
66 | end
67 | currentProvider.trackScope(scope)
68 | end
69 |
70 | return Debugger
--------------------------------------------------------------------------------
/src/Graph/castToGraph.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Returns the input *only* if it is a graph object.
8 | ]]
9 |
10 | local Package = script.Parent.Parent
11 | local Types = require(Package.Types)
12 |
13 | local function castToGraph(
14 | target: any
15 | ): Types.GraphObject?
16 | if
17 | typeof(target) == "table" and
18 | typeof(target.validity) == "string" and
19 | typeof(target.timeliness) == "string" and
20 | typeof(target.dependencySet) == "table" and
21 | typeof(target.dependentSet) == "table"
22 | then
23 | return target
24 | else
25 | return nil
26 | end
27 | end
28 |
29 | return castToGraph
--------------------------------------------------------------------------------
/src/Graph/depend.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Forms a dependency on a graph object.
8 | ]]
9 |
10 | local Package = script.Parent.Parent
11 | local Types = require(Package.Types)
12 | local External = require(Package.External)
13 | local evaluate = require(Package.Graph.evaluate)
14 | local nameOf = require(Package.Utility.nameOf)
15 |
16 | local function depend(
17 | dependent: Types.GraphObject,
18 | dependency: Types.GraphObject
19 | ): ()
20 | -- Ensure dependencies are evaluated and up-to-date
21 | -- when they are depended on. Also, newly created objects
22 | -- might not have any transitive dependencies captured yet,
23 | -- so ensure that they're present.
24 | evaluate(dependency, false)
25 |
26 | if table.isfrozen(dependent.dependencySet) or table.isfrozen(dependency.dependentSet) then
27 | External.logError("cannotDepend", nil, nameOf(dependent, "Dependent"), nameOf(dependency, "dependency"))
28 | end
29 | dependency.dependentSet[dependent] = true
30 | dependent.dependencySet[dependency] = true
31 | end
32 |
33 | return depend
--------------------------------------------------------------------------------
/src/Graph/evaluate.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Evaluates the graph object if necessary, so that it is up to date.
8 | Returns true if it meaningfully changed.
9 |
10 | https://fluff.blog/2024/04/16/monotonic-painting.html
11 | ]]
12 |
13 | local Package = script.Parent.Parent
14 | local Types = require(Package.Types)
15 | local External = require(Package.External)
16 |
17 | local function evaluate(
18 | target: Types.GraphObject,
19 | forceComputation: boolean
20 | ): boolean
21 | if target.validity == "busy" then
22 | return External.logError("infiniteLoop")
23 | end
24 | local firstEvaluation = target.lastChange == nil
25 | local isInvalid = target.validity == "invalid"
26 | if firstEvaluation or isInvalid or forceComputation then
27 | local needsComputation = firstEvaluation or forceComputation
28 | if not needsComputation then
29 | for dependency in target.dependencySet do
30 | evaluate(dependency, false)
31 | if dependency.lastChange > target.lastChange then
32 | needsComputation = true
33 | break
34 | end
35 | end
36 | end
37 | local targetMeaningfullyChanged = false
38 | if needsComputation then
39 | for dependency in target.dependencySet do
40 | dependency.dependentSet[target] = nil
41 | target.dependencySet[dependency] = nil
42 | end
43 | target.validity = "busy"
44 | targetMeaningfullyChanged = target:_evaluate() or firstEvaluation
45 | end
46 | if targetMeaningfullyChanged then
47 | target.lastChange = os.clock()
48 | end
49 | target.validity = "valid"
50 | return targetMeaningfullyChanged
51 | else
52 | return false
53 | end
54 | end
55 |
56 | return evaluate
--------------------------------------------------------------------------------
/src/Instances/Attribute.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | A special key for property tables, which allows users to apply custom
8 | attributes to instances
9 | ]]
10 |
11 | local Package = script.Parent.Parent
12 | local Types = require(Package.Types)
13 | -- Memory
14 | local checkLifetime = require(Package.Memory.checkLifetime)
15 | -- Graph
16 | local Observer = require(Package.Graph.Observer)
17 | -- State
18 | local castToState = require(Package.State.castToState)
19 | local peek = require(Package.State.peek)
20 |
21 | local keyCache: {[string]: Types.SpecialKey} = {}
22 |
23 | local function Attribute(
24 | attributeName: string
25 | ): Types.SpecialKey
26 | local key = keyCache[attributeName]
27 | if key == nil then
28 | key = {
29 | type = "SpecialKey",
30 | kind = "Attribute",
31 | stage = "self",
32 | apply = function(
33 | self: Types.SpecialKey,
34 | scope: Types.Scope,
35 | value: unknown,
36 | applyTo: Instance
37 | )
38 | if castToState(value) then
39 | local value = value :: Types.StateObject
40 | checkLifetime.bOutlivesA(
41 | scope, applyTo,
42 | value.scope, value.oldestTask,
43 | checkLifetime.formatters.boundAttribute, attributeName
44 | )
45 | Observer(scope, value :: any):onBind(function()
46 | applyTo:SetAttribute(attributeName, peek(value))
47 | end)
48 | else
49 | applyTo:SetAttribute(attributeName, value)
50 | end
51 | end
52 | }
53 | keyCache[attributeName] = key
54 | end
55 | return key
56 | end
57 |
58 | return Attribute
--------------------------------------------------------------------------------
/src/Instances/AttributeChange.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | A special key for property tables, which allows users to connect to
8 | an attribute change on an instance.
9 | ]]
10 |
11 | local Package = script.Parent.Parent
12 | local Types = require(Package.Types)
13 | local External = require(Package.External)
14 |
15 | local keyCache: {[string]: Types.SpecialKey} = {}
16 |
17 | local function AttributeChange(
18 | attributeName: string
19 | ): Types.SpecialKey
20 | local key = keyCache[attributeName]
21 | if key == nil then
22 | key = {
23 | type = "SpecialKey",
24 | kind = "AttributeChange",
25 | stage = "observer",
26 | apply = function(
27 | self: Types.SpecialKey,
28 | scope: Types.Scope,
29 | value: unknown,
30 | applyTo: Instance
31 | )
32 | if typeof(value) ~= "function" then
33 | External.logError("invalidAttributeChangeHandler", nil, attributeName)
34 | end
35 | local value = value :: (...unknown) -> (...unknown)
36 | local event = applyTo:GetAttributeChangedSignal(attributeName)
37 | table.insert(scope, event:Connect(function()
38 | value((applyTo :: any):GetAttribute(attributeName))
39 | end))
40 | end
41 | }
42 | keyCache[attributeName] = key
43 | end
44 | return key
45 | end
46 |
47 | return AttributeChange
--------------------------------------------------------------------------------
/src/Instances/AttributeOut.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | A special key for property tables, which allows users to save instance attributes
8 | into state objects
9 | ]]
10 |
11 | local Package = script.Parent.Parent
12 | local Types = require(Package.Types)
13 | local External = require(Package.External)
14 | -- Memory
15 | local checkLifetime = require(Package.Memory.checkLifetime)
16 | -- State
17 | local castToState = require(Package.State.castToState)
18 |
19 | local keyCache: {[string]: Types.SpecialKey} = {}
20 |
21 | local function AttributeOut(
22 | attributeName: string
23 | ): Types.SpecialKey
24 | local key = keyCache[attributeName]
25 | if key == nil then
26 | key = {
27 | type = "SpecialKey",
28 | kind = "AttributeOut",
29 | stage = "observer",
30 | apply = function(
31 | self: Types.SpecialKey,
32 | scope: Types.Scope,
33 | value: unknown,
34 | applyTo: Instance
35 | )
36 | local event = applyTo:GetAttributeChangedSignal(attributeName)
37 |
38 | if not castToState(value) then
39 | External.logError("invalidAttributeOutType")
40 | end
41 | local value = value :: Types.StateObject
42 | if value.kind ~= "Value" then
43 | External.logError("invalidAttributeOutType")
44 | end
45 | local value = value :: Types.Value
46 | checkLifetime.bOutlivesA(
47 | scope, applyTo,
48 | value.scope, value.oldestTask,
49 | checkLifetime.formatters.attributeOutputsTo, attributeName
50 | )
51 |
52 | value:set((applyTo :: any):GetAttribute(attributeName))
53 | table.insert(scope, event:Connect(function()
54 | value:set((applyTo :: any):GetAttribute(attributeName))
55 | end))
56 | end
57 | }
58 | keyCache[attributeName] = key
59 | end
60 | return key
61 | end
62 |
63 | return AttributeOut
64 |
--------------------------------------------------------------------------------
/src/Instances/Child.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Helper function for type checking purposes. Casts the input to a `Child`
8 | type, while constraining the input to be an array of `Child` - this prevents
9 | Luau from erroneously inferring a different array type for the input.
10 | ]]
11 |
12 | local Package = script.Parent.Parent
13 | local Types = require(Package.Types)
14 |
15 | local function Child(
16 | x: {Types.Child}
17 | ): Types.Child
18 | return x
19 | end
20 |
21 | return Child
22 |
--------------------------------------------------------------------------------
/src/Instances/Hydrate.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Processes and returns an existing instance, with options for setting
8 | properties, event handlers and other attributes on the instance.
9 | ]]
10 |
11 | local Package = script.Parent.Parent
12 | local Types = require(Package.Types)
13 | local applyInstanceProps = require(Package.Instances.applyInstanceProps)
14 |
15 | local function Hydrate(
16 | scope: Types.Scope,
17 | target: Instance
18 | )
19 | return function(
20 | props: Types.PropertyTable
21 | ): Instance
22 |
23 | table.insert(scope, target)
24 | applyInstanceProps(scope, props, target)
25 | return target
26 | end
27 | end
28 |
29 | return Hydrate
--------------------------------------------------------------------------------
/src/Instances/New.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Constructs and returns a new instance, with options for setting properties,
8 | event handlers and other attributes on the instance right away.
9 | ]]
10 |
11 | local Package = script.Parent.Parent
12 | local Types = require(Package.Types)
13 | local External = require(Package.External)
14 | local defaultProps = require(Package.Instances.defaultProps)
15 | local applyInstanceProps = require(Package.Instances.applyInstanceProps)
16 |
17 | type Component = (Types.PropertyTable) -> Instance
18 |
19 | local function New(
20 | scope: Types.Scope,
21 | className: string
22 | )
23 | -- This might look appealing to try and cache. But please don't. The scope
24 | -- upvalue is shared between the two curried function calls, so this will
25 | -- open incredible cross-codebase wormholes like you've never seen before.
26 | return function(
27 | props: Types.PropertyTable
28 | ): Instance
29 | local ok, instance = pcall(Instance.new, className)
30 | if not ok then
31 | External.logError("cannotCreateClass", nil, className)
32 | end
33 |
34 | local classDefaults = defaultProps[className]
35 | if classDefaults ~= nil then
36 | for defaultProp, defaultValue in pairs(classDefaults) do
37 | (instance :: any)[defaultProp] = defaultValue
38 | end
39 | end
40 |
41 | table.insert(scope, instance)
42 | applyInstanceProps(scope, props, instance)
43 |
44 | return instance
45 | end
46 | end
47 |
48 | return New
--------------------------------------------------------------------------------
/src/Instances/OnChange.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Constructs special keys for property tables which connect property change
8 | listeners to an instance.
9 | ]]
10 |
11 | local Package = script.Parent.Parent
12 | local Types = require(Package.Types)
13 | local External = require(Package.External)
14 |
15 | local keyCache: {[string]: Types.SpecialKey} = {}
16 |
17 | local function OnChange(
18 | propertyName: string
19 | ): Types.SpecialKey
20 | local key = keyCache[propertyName]
21 | if key == nil then
22 | key = {
23 | type = "SpecialKey",
24 | kind = "OnChange",
25 | stage = "observer",
26 | apply = function(
27 | self: Types.SpecialKey,
28 | scope: Types.Scope,
29 | callback: unknown,
30 | applyTo: Instance
31 | )
32 | local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName)
33 | if not ok then
34 | External.logError("cannotConnectChange", nil, applyTo.ClassName, propertyName)
35 | elseif typeof(callback) ~= "function" then
36 | External.logError("invalidChangeHandler", nil, propertyName)
37 | else
38 | local callback = callback :: (...unknown) -> (...unknown)
39 | table.insert(scope, event:Connect(function()
40 | callback((applyTo :: any)[propertyName])
41 | end))
42 | end
43 | end
44 | }
45 | keyCache[propertyName] = key
46 | end
47 | return key
48 | end
49 |
50 | return OnChange
--------------------------------------------------------------------------------
/src/Instances/OnEvent.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Constructs special keys for property tables which connect event listeners to
8 | an instance.
9 | ]]
10 |
11 | local Package = script.Parent.Parent
12 | local Types = require(Package.Types)
13 | local External = require(Package.External)
14 |
15 | local keyCache: {[string]: Types.SpecialKey} = {}
16 |
17 | local function getProperty_unsafe(
18 | instance: Instance,
19 | property: string
20 | )
21 | return (instance :: any)[property]
22 | end
23 |
24 | local function OnEvent(
25 | eventName: string
26 | ): Types.SpecialKey
27 | local key = keyCache[eventName]
28 | if key == nil then
29 | key = {
30 | type = "SpecialKey",
31 | kind = "OnEvent",
32 | stage = "observer",
33 | apply = function(
34 | self: Types.SpecialKey,
35 | scope: Types.Scope,
36 | callback: unknown,
37 | applyTo: Instance
38 | )
39 | local ok, event = pcall(getProperty_unsafe, applyTo, eventName)
40 | if not ok or typeof(event) ~= "RBXScriptSignal" then
41 | External.logError("cannotConnectEvent", nil, applyTo.ClassName, eventName)
42 | elseif typeof(callback) ~= "function" then
43 | External.logError("invalidEventHandler", nil, eventName)
44 | else
45 | table.insert(scope, event:Connect(callback :: any))
46 | end
47 | end
48 | }
49 | keyCache[eventName] = key
50 | end
51 | return key
52 | end
53 |
54 | return OnEvent
--------------------------------------------------------------------------------
/src/Instances/Out.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | A special key for property tables, which allows users to extract values from
8 | an instance into an automatically-updated Value object.
9 | ]]
10 |
11 | local Package = script.Parent.Parent
12 | local Types = require(Package.Types)
13 | local External = require(Package.External)
14 | -- Memory
15 | local checkLifetime = require(Package.Memory.checkLifetime)
16 | -- State
17 | local castToState = require(Package.State.castToState)
18 |
19 | local keyCache: {[string]: Types.SpecialKey} = {}
20 |
21 | local function Out(
22 | propertyName: string
23 | ): Types.SpecialKey
24 | local key = keyCache[propertyName]
25 | if key == nil then
26 | key = {
27 | type = "SpecialKey",
28 | kind = "Out",
29 | stage = "observer",
30 | apply = function(
31 | self: Types.SpecialKey,
32 | scope: Types.Scope,
33 | value: unknown,
34 | applyTo: Instance
35 | )
36 | local ok, event = pcall(applyTo.GetPropertyChangedSignal, applyTo, propertyName)
37 | if not ok then
38 | External.logError("invalidOutProperty", nil, applyTo.ClassName, propertyName)
39 | end
40 |
41 | if not castToState(value) then
42 | External.logError("invalidOutType")
43 | end
44 | local value = value :: Types.StateObject
45 | if value.kind ~= "Value" then
46 | External.logError("invalidOutType")
47 | end
48 | local value = value :: Types.Value
49 | checkLifetime.bOutlivesA(
50 | scope, applyTo,
51 | value.scope, value.oldestTask,
52 | checkLifetime.formatters.propertyOutputsTo, propertyName
53 | )
54 |
55 | value:set((applyTo :: any)[propertyName])
56 | table.insert(
57 | scope,
58 | event:Connect(function()
59 | value:set((applyTo :: any)[propertyName])
60 | end)
61 | )
62 | end
63 | }
64 | keyCache[propertyName] = key
65 | end
66 | return key
67 | end
68 |
69 | return Out
70 |
--------------------------------------------------------------------------------
/src/Logging/formatError.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Formats a Fusion-specific error message.
8 | ]]
9 |
10 | local Package = script.Parent.Parent
11 | local Types = require(Package.Types)
12 | local messages = require(Package.Logging.messages)
13 |
14 | local ERROR_INFO_URL = "https://elttob.uk/Fusion/0.3/api-reference/general/errors/#"
15 |
16 | local function formatError(
17 | externalProvider: Types.ExternalProvider?,
18 | messageID: string,
19 | errorOrTrace: Types.Error | string | nil,
20 | ...: unknown
21 | ): string
22 | local originalMessageID = messageID
23 | local error: Types.Error? = if typeof(errorOrTrace) == "table" then errorOrTrace else nil
24 | local trace: string? = if typeof(errorOrTrace) == "table" then errorOrTrace.trace else errorOrTrace
25 | local messageText = messages[messageID]
26 | if messageText == nil then
27 | messageID = "unknownMessage"
28 | messageText = messages[messageID]
29 | end
30 | messageText = messageText:format(...)
31 | if error ~= nil then
32 | messageText = messageText:gsub("ERROR_MESSAGE", error.message)
33 | if error.context ~= nil then
34 | messageText ..= ` ({error.context})`
35 | end
36 | else
37 | messageText = messageText:gsub("ERROR_MESSAGE", originalMessageID)
38 | end
39 | messageText = `[Fusion] {messageText} \nID: {messageID}`
40 | if externalProvider ~= nil and externalProvider.policies.allowWebLinks then
41 | messageText ..= `\nLearn more: {ERROR_INFO_URL}{messageID:lower()}`
42 | end
43 | if trace ~= nil then
44 | messageText ..= ` \n---- Stack trace ----\n{trace}`
45 | end
46 | return messageText:gsub("\n", "\n ")
47 | end
48 |
49 | return formatError
--------------------------------------------------------------------------------
/src/Logging/parseError.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | An xpcall() error handler to collect and parse useful information about
8 | errors, such as clean messages and stack traces.
9 | ]]
10 |
11 | local Package = script.Parent.Parent
12 | local Types = require(Package.Types)
13 |
14 | local function parseError(
15 | err: string
16 | ): Types.Error
17 | return {
18 | type = "Error",
19 | raw = err,
20 | message = err:gsub("^.+:%d+:%s*", ""),
21 | trace = debug.traceback(nil, 2)
22 | }
23 | end
24 |
25 | return parseError
--------------------------------------------------------------------------------
/src/Memory/deriveScope.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Creates an empty scope with the same metatables as the original scope. Used
8 | for preserving access to constructors when creating inner scopes.
9 |
10 | This is the public version of the function, which implements external
11 | debugging hooks.
12 | ]]
13 | local Package = script.Parent.Parent
14 | local Types = require(Package.Types)
15 | local ExternalDebug = require(Package.ExternalDebug)
16 | local deriveScopeImpl = require(Package.Memory.deriveScopeImpl)
17 |
18 | local function deriveScope(...)
19 | local scope = deriveScopeImpl(...)
20 | ExternalDebug.trackScope(scope)
21 | return scope
22 | end
23 |
24 | return deriveScope :: Types.DeriveScopeConstructor
--------------------------------------------------------------------------------
/src/Memory/deriveScopeImpl.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Creates an empty scope with the same metatables as the original scope. Used
8 | for preserving access to constructors when creating inner scopes.
9 |
10 | This is the internal version of the function, which does not implement
11 | external debugging hooks.
12 | ]]
13 | local Package = script.Parent.Parent
14 | local Types = require(Package.Types)
15 | local merge = require(Package.Utility.merge)
16 |
17 | -- This return type is technically a lie, but it's required for useful type
18 | -- checking behaviour.
19 | local function deriveScopeImpl(
20 | existing: Types.Scope,
21 | methods: {[unknown]: unknown}?,
22 | ...: {[unknown]: unknown}
23 | ): any
24 | local metatable = getmetatable(existing)
25 | if methods ~= nil then
26 | metatable = table.clone(metatable)
27 | metatable.__index = merge(
28 | true, {},
29 | metatable.__index,
30 | merge(
31 | false, {},
32 | methods,
33 | ...
34 | )
35 | )
36 | end
37 | return setmetatable({}, metatable)
38 | end
39 |
40 | return (deriveScopeImpl :: any) :: Types.DeriveScopeConstructor
--------------------------------------------------------------------------------
/src/Memory/doCleanup.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Cleans up the tasks passed in as the arguments.
8 | A task can be any of the following:
9 |
10 | - an Instance - will be destroyed
11 | - an RBXScriptConnection - will be disconnected
12 | - a function - will be run
13 | - a table with a `Destroy` or `destroy` function - will be called
14 | - an array - `cleanup` will be called on each item
15 | ]]
16 | local Package = script.Parent.Parent
17 | local Types = require(Package.Types)
18 | local External = require(Package.External)
19 | local ExternalDebug = require(Package.ExternalDebug)
20 |
21 | local alreadyDestroying: {[Types.Task]: true} = {}
22 |
23 | local function doCleanup(
24 | task: Types.Task
25 | ): ()
26 | if alreadyDestroying[task] then
27 | return External.logError("destroyedTwice")
28 | end
29 | alreadyDestroying[task] = true
30 |
31 | -- case 1: Instance
32 | if typeof(task) == "Instance" then
33 | task:Destroy()
34 |
35 | -- case 2: RBXScriptConnection
36 | elseif typeof(task) == "RBXScriptConnection" then
37 | task:Disconnect()
38 |
39 | -- case 3: callback
40 | elseif typeof(task) == "function" then
41 | task()
42 |
43 | elseif typeof(task) == "table" then
44 | local task = (task :: any) :: {Destroy: (...unknown) -> (...unknown)?, destroy: (...unknown) -> (...unknown)?}
45 |
46 | -- case 4: destroy() function
47 | if typeof(task.destroy) == "function" then
48 | local task = (task :: any) :: {destroy: (...unknown) -> (...unknown)}
49 | task:destroy()
50 |
51 | -- case 5: Destroy() function
52 | elseif typeof(task.Destroy) == "function" then
53 | local task = (task :: any) :: {Destroy: (...unknown) -> (...unknown)}
54 | task:Destroy()
55 |
56 | -- case 6: table of tasks with an array part
57 | elseif task[1] ~= nil then
58 | local task = task :: {Types.Task}
59 |
60 | -- It is important to iterate backwards through the table, since
61 | -- objects are added in order of construction.
62 | for index = #task, 1, -1 do
63 | doCleanup(task[index])
64 | task[index] = nil
65 | end
66 |
67 | ExternalDebug.untrackScope(task)
68 | end
69 | end
70 |
71 | alreadyDestroying[task] = nil
72 | end
73 |
74 | return doCleanup
--------------------------------------------------------------------------------
/src/Memory/innerScope.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Derives a new scope that's destroyed exactly once, whether by the user or by
8 | the scope that it's inside of.
9 | ]]
10 | local Package = script.Parent.Parent
11 | local Types = require(Package.Types)
12 | local ExternalDebug = require(Package.ExternalDebug)
13 | local deriveScopeImpl = require(Package.Memory.deriveScopeImpl)
14 |
15 | local function innerScope(
16 | existing: Types.Scope,
17 | ...: {[unknown]: unknown}
18 | ): any
19 | local new = deriveScopeImpl(existing, ...)
20 | table.insert(existing, new)
21 | table.insert(
22 | new,
23 | function()
24 | local index = table.find(existing, new)
25 | if index ~= nil then
26 | table.remove(existing, index)
27 | end
28 | end
29 | )
30 | ExternalDebug.trackScope(new)
31 | return new
32 | end
33 |
34 | return (innerScope :: any) :: Types.DeriveScopeConstructor
--------------------------------------------------------------------------------
/src/Memory/insert.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Inserts clean up tasks passed in to the scope.
8 | ]]
9 | local Package = script.Parent.Parent
10 | local Types = require(Package.Types)
11 |
12 | local function insert(
13 | scope: Types.Scope,
14 | ...: Tasks...
15 | ): Tasks...
16 | for index = 1, select("#", ...) do
17 | table.insert(scope, select(index, ...))
18 | end
19 | return ...
20 | end
21 |
22 | return insert
23 |
--------------------------------------------------------------------------------
/src/Memory/needsDestruction.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Returns true if the given value is not automatically memory managed, and
8 | requires manual cleanup.
9 | ]]
10 |
11 | local function needsDestruction(
12 | x: unknown
13 | ): boolean
14 | return typeof(x) == "Instance"
15 | end
16 |
17 | return needsDestruction
--------------------------------------------------------------------------------
/src/Memory/scoped.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Creates cleanup tables with access to constructors as methods.
8 | ]]
9 |
10 | local Package = script.Parent.Parent
11 | local Types = require(Package.Types)
12 | local ExternalDebug = require(Package.ExternalDebug)
13 | local merge = require(Package.Utility.merge)
14 |
15 | local function scoped(
16 | ...: {[unknown]: unknown}
17 | ): any
18 | local metatable = {__index = merge(false, {}, ...)}
19 | local scope = setmetatable({}, metatable) :: any
20 | ExternalDebug.trackScope(scope)
21 | return scope
22 | end
23 |
24 | return (scoped :: any) :: Types.ScopedConstructor
--------------------------------------------------------------------------------
/src/RobloxExternal.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 |
5 | --[[
6 | Roblox implementation for Fusion's abstract provider layer.
7 | ]]
8 |
9 | local RunService = game:GetService("RunService")
10 | local HttpService = game:GetService("HttpService")
11 |
12 | local Package = script.Parent
13 | local External = require(Package.External)
14 |
15 | local RobloxExternal = {}
16 |
17 | RobloxExternal.policies = {
18 | allowWebLinks = RunService:IsStudio()
19 | }
20 |
21 | --[[
22 | Sends an immediate task to the external provider. Throws if none is set.
23 | ]]
24 | function RobloxExternal.doTaskImmediate(
25 | resume: () -> ()
26 | )
27 | task.spawn(resume)
28 | end
29 |
30 | --[[
31 | Sends a deferred task to the external provider. Throws if none is set.
32 | ]]
33 | function RobloxExternal.doTaskDeferred(
34 | resume: () -> ()
35 | )
36 | task.defer(resume)
37 | end
38 |
39 | --[[
40 | Errors in a different thread to preserve the flow of execution.
41 | ]]
42 | function RobloxExternal.logErrorNonFatal(
43 | errorString: string
44 | )
45 | task.spawn(error, errorString, 0)
46 | end
47 |
48 | --[[
49 | Shows a warning message in the output.
50 | ]]
51 | RobloxExternal.logWarn = warn
52 |
53 | --[[
54 | Sends an update step to Fusion using the Roblox clock time.
55 | ]]
56 | local function performUpdateStep()
57 | External.performUpdateStep(os.clock())
58 | end
59 |
60 | --[[
61 | Binds Fusion's update step to RunService step events.
62 | ]]
63 | local stopSchedulerFunc = nil :: (() -> ())?
64 | function RobloxExternal.startScheduler()
65 | if stopSchedulerFunc ~= nil then
66 | return
67 | end
68 | if RunService:IsClient() then
69 | -- In cases where multiple Fusion modules are running simultaneously,
70 | -- this prevents collisions.
71 | local id = "FusionUpdateStep_" .. HttpService:GenerateGUID()
72 | RunService:BindToRenderStep(
73 | id,
74 | Enum.RenderPriority.First.Value,
75 | performUpdateStep
76 | )
77 | stopSchedulerFunc = function()
78 | RunService:UnbindFromRenderStep(id)
79 | end
80 | else
81 | local connection = RunService.Heartbeat:Connect(performUpdateStep)
82 | stopSchedulerFunc = function()
83 | connection:Disconnect()
84 | end
85 | end
86 | end
87 |
88 | --[[
89 | Unbinds Fusion's update step from RunService step events.
90 | ]]
91 | function RobloxExternal.stopScheduler()
92 | if stopSchedulerFunc ~= nil then
93 | stopSchedulerFunc()
94 | stopSchedulerFunc = nil
95 | end
96 | end
97 |
98 | return RobloxExternal
--------------------------------------------------------------------------------
/src/State/For/ForTypes.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Stores types that are commonly used between For objects.
8 | ]]
9 |
10 | local Package = script.Parent.Parent.Parent
11 | local Types = require(Package.Types)
12 |
13 | export type SubObject = {
14 | -- Not all sub objects need to store a scope, for example if the scope
15 | -- remains empty, it'll be given back to the scope pool.
16 | maybeScope: Types.Scope?,
17 | inputKey: KI,
18 | inputValue: VI,
19 | roamKeys: boolean,
20 | roamValues: boolean,
21 | invalidateInputKey: (SubObject) -> (),
22 | invalidateInputValue: (SubObject) -> (),
23 | useOutputPair: (SubObject, Types.Use) -> (KO?, VO?)
24 | }
25 |
26 | export type Disassembly = Types.GraphObject & {
27 | populate: (Disassembly, Types.Use, output: {[KO]: VO}) -> ()
28 | }
29 |
30 | return nil
31 |
--------------------------------------------------------------------------------
/src/State/Value.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | A state object which allows regular Luau code to control its value.
8 |
9 | https://elttob.uk/Fusion/0.3/api-reference/state/types/value/
10 | ]]
11 |
12 | local Package = script.Parent.Parent
13 | local Types = require(Package.Types)
14 | -- Graph
15 | local change = require(Package.Graph.change)
16 | -- Utility
17 | local isSimilar = require(Package.Utility.isSimilar)
18 | local nicknames = require(Package.Utility.nicknames)
19 |
20 | type Self = Types.Value
21 |
22 | local class = {}
23 | class.type = "State"
24 | class.kind = "Value"
25 | class.timeliness = "lazy"
26 | class.dependencySet = table.freeze {}
27 |
28 | local METATABLE = table.freeze {__index = class}
29 |
30 | local function Value(
31 | scope: Types.Scope,
32 | initialValue: T
33 | ): Types.Value
34 | local createdAt = os.clock()
35 | local self: Self = setmetatable(
36 | {
37 | createdAt = createdAt,
38 | dependentSet = {},
39 | lastChange = os.clock(),
40 | scope = scope,
41 | validity = "valid",
42 | _EXTREMELY_DANGEROUS_usedAsValue = initialValue
43 | },
44 | METATABLE
45 | ) :: any
46 | local destroy = function()
47 | self.scope = nil
48 | end
49 | self.oldestTask = destroy
50 | nicknames[self.oldestTask] = "Value"
51 | table.insert(scope, destroy)
52 | return self
53 | end
54 |
55 | function class.set(
56 | self: Self,
57 | newValue: S
58 | ): S
59 | local oldValue = self._EXTREMELY_DANGEROUS_usedAsValue
60 | if not isSimilar(oldValue, newValue) then
61 | self._EXTREMELY_DANGEROUS_usedAsValue = newValue :: any
62 | change(self)
63 | end
64 | return newValue
65 | end
66 |
67 | function class._evaluate(
68 | _self: Self
69 | ): boolean
70 | -- The similarity test is done in advance when the value is set, so this
71 | -- should be fine.
72 | return true
73 | end
74 |
75 | table.freeze(class)
76 | return Value :: Types.ValueConstructor
--------------------------------------------------------------------------------
/src/State/castToState.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Returns the input *only* if it is a state object.
8 | ]]
9 |
10 | local Package = script.Parent.Parent
11 | local Types = require(Package.Types)
12 |
13 | local function castToState(
14 | target: Types.UsedAs
15 | ): Types.StateObject?
16 | if
17 | typeof(target) == "table" and
18 | target.type == "State"
19 | then
20 | return target
21 | else
22 | return nil
23 | end
24 | end
25 |
26 | return castToState
--------------------------------------------------------------------------------
/src/State/peek.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Extracts a value of type T from its input.
8 |
9 | https://elttob.uk/Fusion/0.3/api-reference/state/members/peek/
10 | ]]
11 |
12 | local Package = script.Parent.Parent
13 | local Types = require(Package.Types)
14 | -- State
15 | local castToState = require(Package.State.castToState)
16 | -- Graph
17 | local evaluate = require(Package.Graph.evaluate)
18 |
19 | local function peek(
20 | target: Types.UsedAs
21 | ): T
22 | local targetState = castToState(target)
23 | if targetState ~= nil then
24 | evaluate(targetState, false)
25 | return targetState._EXTREMELY_DANGEROUS_usedAsValue :: T
26 | else
27 | return target :: T
28 | end
29 | end
30 |
31 | return peek
--------------------------------------------------------------------------------
/src/State/updateAll.luau:
--------------------------------------------------------------------------------
1 | return nil -- dummy file so I can write tests
--------------------------------------------------------------------------------
/src/Utility/Contextual.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Time-based contextual values, to allow for transparently passing values down
8 | the call stack.
9 | ]]
10 |
11 | local Package = script.Parent.Parent
12 | local Types = require(Package.Types)
13 | local External = require(Package.External)
14 | -- Logging
15 | local parseError = require(Package.Logging.parseError)
16 |
17 | export type Self = Types.Contextual & {
18 | _valuesNow: {[thread]: {value: T}},
19 | _defaultValue: T
20 | }
21 |
22 | local class = {}
23 | class.type = "Contextual"
24 |
25 | local METATABLE = table.freeze {__index = class}
26 | local WEAK_KEYS_METATABLE = table.freeze {__mode = "k"}
27 |
28 | local function Contextual(
29 | defaultValue: T
30 | ): Types.Contextual
31 | local self: Self = setmetatable(
32 | {
33 | -- if we held strong references to threads here, then if a thread was
34 | -- killed before this contextual had a chance to finish executing its
35 | -- callback, it would be held strongly in this table forever
36 | _valuesNow = setmetatable({}, WEAK_KEYS_METATABLE),
37 | _defaultValue = defaultValue
38 | },
39 | METATABLE
40 | ) :: any
41 |
42 | return self
43 | end
44 |
45 | --[[
46 | Returns the current value of this contextual.
47 | ]]
48 | function class.now(
49 | self: Self
50 | ): T
51 | local thread = coroutine.running()
52 | local value = self._valuesNow[thread]
53 | if typeof(value) ~= "table" then
54 | return self._defaultValue
55 | else
56 | return value.value
57 | end
58 | end
59 |
60 | --[[
61 | Temporarily assigns a value to this contextual.
62 | ]]
63 | function class.is(
64 | self: Self,
65 | newValue: T
66 | )
67 | local methods = {}
68 |
69 | function methods.during(
70 | _: any, -- during is called with colon syntax but we don't care
71 | callback: (A...) -> T,
72 | ...: A...
73 | ): T
74 | local thread = coroutine.running()
75 | local prevValue = self._valuesNow[thread]
76 | -- Storing the value in this format allows us to distinguish storing
77 | -- `nil` from not calling `:during()` at all.
78 | self._valuesNow[thread] = { value = newValue }
79 | local ok, value = xpcall(callback, parseError, ...)
80 | self._valuesNow[thread] = prevValue
81 | if not ok then
82 | External.logError("callbackError", value :: any)
83 | end
84 | return value
85 | end
86 |
87 | return methods
88 | end
89 |
90 | table.freeze(class)
91 | return Contextual
--------------------------------------------------------------------------------
/src/Utility/Safe.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | A variant of xpcall() designed for inline usage, letting you define fallback
8 | values based on caught errors.
9 | ]]
10 |
11 | local Package = script.Parent.Parent
12 |
13 | local function Safe(
14 | callbacks: {
15 | try: () -> Success,
16 | fallback: (err: unknown) -> Fail
17 | }
18 | ): Success | Fail
19 | local _, value = xpcall(callbacks.try, callbacks.fallback)
20 | return value
21 | end
22 |
23 | return Safe
--------------------------------------------------------------------------------
/src/Utility/isSimilar.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Implements the 'similarity test' used to determine whether two values have
8 | a meaningful difference.
9 |
10 | https://elttob.uk/Fusion/0.3/tutorials/best-practices/optimisation/#similarity
11 | ]]
12 |
13 | local function isSimilar(
14 | a: unknown,
15 | b: unknown
16 | ): boolean
17 | local typeA = typeof(a)
18 | local isTable = typeA == "table"
19 | local isUserdata = typeA == "userdata"
20 | return
21 | if not (isTable or isUserdata) then
22 | a == b or a ~= a and b ~= b
23 | elseif typeA == typeof(b) and (isUserdata or table.isfrozen(a :: any) or getmetatable(a :: any) ~= nil) then
24 | a == b
25 | else
26 | false
27 | end
28 |
29 | return isSimilar
30 |
--------------------------------------------------------------------------------
/src/Utility/merge.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Attempts to merge a variadic number of tables together.
8 | ]]
9 |
10 | local Package = script.Parent.Parent
11 | local External = require(Package.External)
12 |
13 | local function merge(
14 | overwrite: boolean,
15 | into: {[unknown]: unknown},
16 | ...: {[unknown]: unknown}
17 | ): {[unknown]: unknown}
18 | local fromTables = {...}
19 | if #fromTables < 1 then
20 | return into
21 | else
22 | for _, fromTable in fromTables do
23 | for key, value in fromTable do
24 | if overwrite or into[key] == nil then
25 | into[key] = value
26 | elseif not overwrite then
27 | External.logError("mergeConflict", nil, tostring(key))
28 | end
29 | end
30 | end
31 | return into
32 | end
33 | end
34 |
35 | return merge
36 |
--------------------------------------------------------------------------------
/src/Utility/nameOf.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Returns the most specific custom name for the given object.
8 | ]]
9 |
10 | local Package = script.Parent.Parent
11 | -- Utility
12 | local nicknames = require(Package.Utility.nicknames)
13 |
14 | local function nameOf(
15 | x: unknown,
16 | defaultName: string
17 | ): string
18 | local nickname = nicknames[x]
19 | if typeof(nickname) == "string" then
20 | return nickname
21 | end
22 | if typeof(x) == "table" then
23 | local x = x :: {[any]: any}
24 | if typeof(x.name) == "string" then
25 | return x.name
26 | elseif typeof(x.kind) == "string" then
27 | return x.kind
28 | elseif typeof(x.type) == "string" then
29 | return x.type
30 | end
31 | end
32 | return defaultName
33 | end
34 |
35 | return nameOf
--------------------------------------------------------------------------------
/src/Utility/never.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Never returns.
8 | ]]
9 |
10 | local function never(): never
11 | error("This codepath should not be reachable")
12 | end
13 |
14 | return never
--------------------------------------------------------------------------------
/src/Utility/nicknames.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Stores nicknames for values that don't support metatables, so that `nameOf`
8 | can return values for them.
9 | ]]
10 |
11 | return setmetatable({}, {__mode = "k"})
--------------------------------------------------------------------------------
/src/Utility/xtypeof.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | --[[
7 | Extended typeof, designed for identifying custom objects.
8 | If given a table with a `type` string, returns that.
9 | Otherwise, returns `typeof()` the argument.
10 | ]]
11 |
12 | local function xtypeof(
13 | x: unknown
14 | ): string
15 | local typeString = typeof(x)
16 |
17 | if typeString == "table" then
18 | local x = x :: {type: unknown?}
19 | if typeof(x.type) == "string" then
20 | return x.type
21 | end
22 | end
23 |
24 | return typeString
25 | end
26 |
27 | return xtypeof
--------------------------------------------------------------------------------
/test-runner.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Fusion Test Runner",
3 | "servePlaceIds": [
4 | 0
5 | ],
6 | "tree": {
7 | "$className": "DataModel",
8 | "ReplicatedStorage": {
9 | "$className": "ReplicatedStorage",
10 | "Fusion": {
11 | "$path": "default.project.json"
12 | }
13 | },
14 | "ServerScriptService": {
15 | "$className": "ServerScriptService",
16 | "FusionTest": {
17 | "$path": "test"
18 | }
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/test/Spec/Instances/Attribute.spec.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | local task = nil -- Disable usage of Roblox's task scheduler
4 |
5 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
6 | local Fusion = ReplicatedStorage.Fusion
7 |
8 | local New = require(Fusion.Instances.New)
9 | local Attribute = require(Fusion.Instances.Attribute)
10 | local Value = require(Fusion.State.Value)
11 | local doCleanup = require(Fusion.Memory.doCleanup)
12 |
13 | return function()
14 | local it = getfenv().it
15 |
16 | it("creates attributes (constant)", function()
17 | local expect = getfenv().expect
18 |
19 | local scope = {}
20 | local child = New(scope, "Folder") {
21 | [Attribute "Foo"] = "Bar"
22 | }
23 | expect(child:GetAttribute("Foo")).to.equal("Bar")
24 | doCleanup(scope)
25 | end)
26 |
27 | it("creates attributes (state)", function()
28 | local expect = getfenv().expect
29 |
30 | local scope = {}
31 | local attributeValue = Value(scope, "Bar")
32 | local child = New(scope, "Folder") {
33 | [Attribute "Foo"] = attributeValue
34 | }
35 | expect(child:GetAttribute("Foo")).to.equal("Bar")
36 | end)
37 |
38 | it("updates attributes when state objects are updated", function()
39 | local expect = getfenv().expect
40 |
41 | local scope = {}
42 | local attributeValue = Value(scope, "Bar")
43 | local child = New(scope, "Folder") {
44 | [Attribute "Foo"] = attributeValue
45 | }
46 | expect(child:GetAttribute("Foo")).to.equal("Bar")
47 | attributeValue:set("Baz")
48 | expect(child:GetAttribute("Foo")).to.equal("Baz")
49 | doCleanup(scope)
50 | end)
51 | end
52 |
--------------------------------------------------------------------------------
/test/Spec/Instances/AttributeChange.spec.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | local task = nil -- Disable usage of Roblox's task scheduler
4 |
5 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
6 | local Fusion = ReplicatedStorage.Fusion
7 |
8 | local New = require(Fusion.Instances.New)
9 | local Attribute = require(Fusion.Instances.Attribute)
10 | local AttributeChange = require(Fusion.Instances.AttributeChange)
11 | local doCleanup = require(Fusion.Memory.doCleanup)
12 |
13 | return function()
14 | local it = getfenv().it
15 | local itSKIP = getfenv().itSKIP
16 |
17 | -- TODO: the event is spawned by Roblox's task scheduler, so it doesn't work
18 | -- with our testing architecture anymore.
19 |
20 | itSKIP("should connect attribute change handlers", function()
21 | local expect = getfenv().expect
22 |
23 | local scope = {}
24 | local changeCount = 0
25 | local child = New(scope, "Folder") {
26 | [Attribute "Foo"] = "Bar",
27 | [AttributeChange "Foo"] = function()
28 | changeCount += 1
29 | end
30 | }
31 |
32 | child:SetAttribute("Foo", "Baz")
33 | expect(changeCount).never.to.equal(0)
34 | doCleanup(scope)
35 | end)
36 |
37 | -- TODO: the event is spawned by Roblox's task scheduler, so it doesn't work
38 | -- with our testing architecture anymore.
39 |
40 | itSKIP("should pass the updated value as an argument", function()
41 | local expect = getfenv().expect
42 |
43 | local scope = {}
44 | local updatedValue = ""
45 | local updated = false
46 | local child = New(scope, "Folder") {
47 | [AttributeChange "Foo"] = function(newValue)
48 | updatedValue = newValue
49 | updated = true
50 | end
51 | }
52 |
53 | child:SetAttribute("Foo", "Baz")
54 | expect(updatedValue).to.equal("Baz")
55 | doCleanup(scope)
56 | end)
57 |
58 | it("should error when given an invalid handler", function()
59 | local expect = getfenv().expect
60 |
61 | expect(function()
62 | local scope = {}
63 | New(scope, "Folder") {
64 | [AttributeChange "Foo"] = 0
65 | }
66 | doCleanup(scope)
67 | end).to.throw("invalidAttributeChangeHandler")
68 | end)
69 | end
70 |
--------------------------------------------------------------------------------
/test/Spec/Instances/Hydrate.spec.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | local task = nil -- Disable usage of Roblox's task scheduler
4 |
5 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
6 | local Fusion = ReplicatedStorage.Fusion
7 |
8 | local Hydrate = require(Fusion.Instances.Hydrate)
9 | local doCleanup = require(Fusion.Memory.doCleanup)
10 |
11 | return function()
12 | local it = getfenv().it
13 |
14 | it("should return the instance it was passed", function()
15 | local expect = getfenv().expect
16 |
17 | local scope = {}
18 | local ins = Instance.new("Folder")
19 | expect(Hydrate(scope, ins) {}).to.equal(ins)
20 | doCleanup(scope)
21 | end)
22 |
23 | it("should apply properties to the instance", function()
24 | local expect = getfenv().expect
25 |
26 | local scope = {}
27 | local ins = Instance.new("Folder")
28 | Hydrate(scope, ins) {
29 | Name = "Jeremy"
30 | }
31 | expect(ins.Name).to.equal("Jeremy")
32 | doCleanup(scope)
33 | end)
34 | end
35 |
--------------------------------------------------------------------------------
/test/Spec/Instances/New.spec.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | local task = nil -- Disable usage of Roblox's task scheduler
4 |
5 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
6 | local Fusion = ReplicatedStorage.Fusion
7 |
8 | local New = require(Fusion.Instances.New)
9 | local defaultProps = require(Fusion.Instances.defaultProps)
10 | local doCleanup = require(Fusion.Memory.doCleanup)
11 |
12 | return function()
13 | local it = getfenv().it
14 |
15 | it("should create a new instance", function()
16 | local expect = getfenv().expect
17 |
18 | local scope = {}
19 | local ins = New (scope, "Frame") {}
20 | expect(typeof(ins) == "Instance").to.be.ok()
21 | expect(ins:IsA("Frame")).to.be.ok()
22 | end)
23 |
24 | it("should throw for non-existent class types", function()
25 | local expect = getfenv().expect
26 |
27 | expect(function()
28 | local scope = {}
29 | New (scope, "This is not a valid class type") {}
30 | doCleanup(scope)
31 | end).to.throw("cannotCreateClass")
32 | end)
33 |
34 | it("should apply 'sensible default' properties", function()
35 | local expect = getfenv().expect
36 |
37 | for className, defaults in pairs(defaultProps) do
38 | local scope = {}
39 | local ins = New (scope, className) {}
40 | for propName, propValue in pairs(defaults) do
41 | expect((ins :: any)[propName]).to.equal(propValue)
42 | end
43 | doCleanup(scope)
44 | end
45 | end)
46 |
47 | it("doesn't incorrectly cache scope between invocations", function()
48 | local expect = getfenv().expect
49 |
50 | local scope1 = {}
51 | local scope2 = {}
52 | New (scope1, "Frame") {}
53 | New (scope2, "Frame") {}
54 | expect(#scope1).to.equal(1)
55 | expect(#scope2).to.equal(1)
56 |
57 | doCleanup(scope1)
58 | doCleanup(scope2)
59 | end)
60 | end
61 |
--------------------------------------------------------------------------------
/test/Spec/Instances/Out.spec.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | local task = nil -- Disable usage of Roblox's task scheduler
4 |
5 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
6 | local Fusion = ReplicatedStorage.Fusion
7 |
8 | local New = require(Fusion.Instances.New)
9 | local Out = require(Fusion.Instances.Out)
10 | local Value = require(Fusion.State.Value)
11 | local peek = require(Fusion.State.peek)
12 | local doCleanup = require(Fusion.Memory.doCleanup)
13 |
14 | return function()
15 | local it = getfenv().it
16 | local itSKIP = getfenv().itSKIP
17 |
18 | -- TODO: the event is spawned by Roblox's task scheduler, so it doesn't work
19 | -- with our testing architecture anymore.
20 |
21 | itSKIP("should reflect external property changes", function()
22 | local expect = getfenv().expect
23 |
24 | local scope = {}
25 | local outValue = Value(scope, nil)
26 |
27 | local child = New(scope, "Folder") {
28 | [Out "Name"] = outValue
29 | }
30 | expect(peek(outValue)).to.equal("Folder")
31 |
32 | child.Name = "Mary"
33 | expect(peek(outValue)).to.equal("Mary")
34 | doCleanup(scope)
35 | end)
36 |
37 | -- TODO: the event is spawned by Roblox's task scheduler, so it doesn't work
38 | -- with our testing architecture anymore.
39 |
40 | itSKIP("should reflect property changes from bound state", function()
41 | local expect = getfenv().expect
42 |
43 | local scope = {}
44 | local outValue = Value(scope, nil)
45 | local inValue = Value(scope, "Gabriel")
46 |
47 | New(scope, "Folder") {
48 | Name = inValue,
49 | [Out "Name"] = outValue
50 | }
51 | expect(peek(outValue)).to.equal("Gabriel")
52 |
53 | inValue:set("Joseph")
54 | expect(peek(outValue)).to.equal("Joseph")
55 | doCleanup(scope)
56 | end)
57 |
58 | -- TODO: the event is spawned by Roblox's task scheduler, so it doesn't work
59 | -- with our testing architecture anymore.
60 |
61 | itSKIP("should support two-way data binding", function()
62 | local expect = getfenv().expect
63 |
64 | local scope = {}
65 | local twoWayValue = Value(scope, "Gabriel")
66 |
67 | local child = New(scope, "Folder") {
68 | Name = twoWayValue,
69 | [Out "Name"] = twoWayValue
70 | }
71 | expect(peek(twoWayValue)).to.equal("Gabriel")
72 |
73 | twoWayValue:set("Joseph")
74 | expect(child.Name).to.equal("Joseph")
75 |
76 | child.Name = "Elias"
77 | expect(peek(twoWayValue)).to.equal("Elias")
78 | doCleanup(scope)
79 | end)
80 | end
81 |
--------------------------------------------------------------------------------
/test/Spec/Memory/insert.spec.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | local task = nil -- Disable usage of Roblox's task scheduler
4 |
5 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
6 | local Fusion = ReplicatedStorage.Fusion
7 | local scoped = require(Fusion.Memory.scoped)
8 | local doCleanup = require(Fusion.Memory.doCleanup)
9 | local insert = require(Fusion.Memory.insert)
10 |
11 | return function()
12 | local it = getfenv().it
13 |
14 | it("should accept zero tasks", function()
15 | local expect = getfenv().expect
16 |
17 | local scope = scoped()
18 | insert(scope)
19 |
20 | expect(#scope).to.equal(0)
21 | end)
22 |
23 | it("should accept single tasks", function()
24 | local expect = getfenv().expect
25 |
26 | local isDestroyed = false
27 | local scope = scoped()
28 | insert(scope, function()
29 | isDestroyed = true
30 | end)
31 |
32 | expect(#scope).to.equal(1)
33 | doCleanup(scope)
34 |
35 | expect(isDestroyed).to.equal(true)
36 | end)
37 |
38 | it("should accept multiple tasks", function()
39 | local expect = getfenv().expect
40 |
41 | local counter = 0
42 | local scope = scoped()
43 |
44 | insert(scope, function()
45 | counter += 1
46 | end)
47 | insert(scope, function()
48 | counter += 1
49 | end)
50 | insert(scope, {
51 | function()
52 | counter += 1
53 | end
54 | })
55 |
56 | expect(#scope).to.equal(3)
57 | doCleanup(scope)
58 | expect(counter).to.equal(3)
59 | end)
60 |
61 | it("should return the given tasks", function()
62 | local expect = getfenv().expect
63 |
64 | local counter = 0
65 | local function onDestroy()
66 | counter += 1
67 | end
68 | local function onDestroy2()
69 | counter += 2
70 | end
71 | local scope = scoped()
72 |
73 | local returnedDestroy, returnedDestroy2 = insert(scope, onDestroy, onDestroy2)
74 | expect(returnedDestroy).to.equal(onDestroy)
75 | expect(returnedDestroy2).to.equal(onDestroy2)
76 | expect(#scope).to.equal(2)
77 | doCleanup(scope)
78 | expect(counter).to.equal(3)
79 | end)
80 | end
81 |
--------------------------------------------------------------------------------
/test/Spec/Utility/Safe.spec.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | local task = nil -- Disable usage of Roblox's task scheduler
4 |
5 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
6 | local Fusion = ReplicatedStorage.Fusion
7 |
8 | local Safe = require(Fusion.Utility.Safe)
9 |
10 | return function()
11 | local it = getfenv().it
12 |
13 | it("returns values from try() on success", function()
14 | local expect = getfenv().expect
15 |
16 | expect(
17 | Safe {
18 | try = function()
19 | return "foo"
20 | end,
21 | fallback = function()
22 | return "bar"
23 | end
24 | }
25 | ).to.equal("foo")
26 | end)
27 |
28 | it("returns values from fallback() on error", function()
29 | local expect = getfenv().expect
30 |
31 | expect(
32 | Safe {
33 | try = function()
34 | error("garb", 0)
35 | return "foo"
36 | end,
37 | fallback = function()
38 | return "bar"
39 | end
40 | }
41 | ).to.equal("bar")
42 | end)
43 |
44 | it("passes the error on to fallback()", function()
45 | local expect = getfenv().expect
46 |
47 | expect(
48 | Safe {
49 | try = function()
50 | error("garb", 0)
51 | return "foo"
52 | end,
53 | fallback = function(err)
54 | return "bar" .. tostring(err)
55 | end
56 | }
57 | ).to.equal("bargarb")
58 | end)
59 | end
--------------------------------------------------------------------------------
/test/Spec/_Integration/DynamicGraphs.spec.lua:
--------------------------------------------------------------------------------
1 | --!strict
2 | --!nolint LocalUnused
3 | --!nolint LocalShadow
4 | local task = nil -- Disable usage of Roblox's task scheduler
5 |
6 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
7 | local Fusion = require(ReplicatedStorage.Fusion)
8 | local scoped, peek = Fusion.scoped, Fusion.peek
9 |
10 | return function()
11 | local describe = getfenv().describe
12 |
13 | describe("regression tests", function()
14 | local it = getfenv().it
15 |
16 | it("re-entrant Observers do not block eager updates", function()
17 | local expect = getfenv().expect
18 |
19 | local scope = scoped(Fusion)
20 |
21 | local count = 0
22 | local unrelatedValue = scope:Value(count)
23 | local trigger = scope:Value(false)
24 |
25 | local o1 = scope:Observer(trigger)
26 | o1:onChange(function()
27 | count += 1
28 | unrelatedValue:set(count)
29 | end)
30 |
31 | local numFires = 0
32 | local o2 = scope:Observer(trigger)
33 | o2:onChange(function()
34 | numFires += 1
35 | end)
36 |
37 | trigger:set(true)
38 | expect(numFires).to.equal(1)
39 | trigger:set(false)
40 | expect(numFires).to.equal(2)
41 | trigger:set(true)
42 | expect(numFires).to.equal(3)
43 |
44 | scope:doCleanup()
45 | end)
46 | end)
47 | end
--------------------------------------------------------------------------------
/test/SpecExternal.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
4 | local Fusion = ReplicatedStorage.Fusion
5 |
6 | local External = require(Fusion.External)
7 |
8 | local SpecExternal = {}
9 |
10 | SpecExternal.policies = {
11 | allowWebLinks = true
12 | }
13 |
14 | local queue = {} :: {thread}
15 |
16 | --[[
17 | Sends an immediate task to the external scheduler. Throws if none is set.
18 | ]]
19 | function SpecExternal.doTaskImmediate(
20 | resume: () -> ()
21 | )
22 | table.insert(queue, 1, coroutine.running())
23 | table.insert(queue, 1, coroutine.create(resume))
24 | coroutine.yield()
25 | end
26 |
27 | --[[
28 | Sends a deferred task to the external scheduler. Throws if none is set.
29 | ]]
30 | function SpecExternal.doTaskDeferred(
31 | resume: () -> ()
32 | )
33 | table.insert(queue, coroutine.create(resume))
34 | end
35 |
36 | function SpecExternal.deferSelfForTesting()
37 | table.insert(queue, coroutine.running())
38 | coroutine.yield()
39 | end
40 |
41 | --[[
42 | Errors in a different thread to preserve the flow of execution.
43 | ]]
44 | function SpecExternal.logErrorNonFatal(
45 | errorString: string
46 | ): ()
47 | -- silently discard
48 | end
49 |
50 | --[[
51 | Shows a warning message in the output.
52 | ]]
53 | function SpecExternal.logWarn(
54 | errorString: string
55 | ): ()
56 | -- silently discard
57 | end
58 |
59 | local doUpdateSteps = false
60 |
61 | --[[
62 | Binds Fusion's update step to RunService step events.
63 | ]]
64 | function SpecExternal.startScheduler()
65 | doUpdateSteps = true
66 | end
67 |
68 | --[[
69 | Unbinds Fusion's update step from RunService step events.
70 | ]]
71 | function SpecExternal.stopScheduler()
72 | doUpdateSteps = false
73 | end
74 |
75 | --[[
76 | Unbinds Fusion's update step from RunService step events.
77 | ]]
78 | function SpecExternal.step(
79 | currentTime: number
80 | )
81 | if doUpdateSteps then
82 | External.performUpdateStep(currentTime)
83 | end
84 |
85 | while true do
86 | local nextTask = table.remove(queue, 1)
87 | if nextTask == nil then
88 | break
89 | end
90 | local ok, result: string = coroutine.resume(nextTask)
91 | if not ok then
92 | warn("Error in spec scheduler: " .. result)
93 | end
94 | end
95 | end
96 |
97 | return SpecExternal
--------------------------------------------------------------------------------
/test/TestEZ/Context.luau:
--------------------------------------------------------------------------------
1 | --[[
2 | The Context object implements a write-once key-value store. It also allows
3 | for a new Context object to inherit the entries from an existing one.
4 | ]]
5 | local Context = {}
6 |
7 | function Context.new(parent)
8 | local meta = {}
9 | local index = {}
10 | meta.__index = index
11 |
12 | if parent then
13 | for key, value in pairs(getmetatable(parent).__index) do
14 | index[key] = value
15 | end
16 | end
17 |
18 | function meta.__newindex(_obj, key, value)
19 | assert(index[key] == nil, string.format("Cannot reassign %s in context", tostring(key)))
20 | index[key] = value
21 | end
22 |
23 | return setmetatable({}, meta)
24 | end
25 |
26 | return Context
27 |
--------------------------------------------------------------------------------
/test/TestEZ/ExpectationContext.luau:
--------------------------------------------------------------------------------
1 | local Expectation = require(script.Parent.Expectation)
2 | local checkMatcherNameCollisions = Expectation.checkMatcherNameCollisions
3 |
4 | local function copy(t)
5 | local result = {}
6 |
7 | for key, value in pairs(t) do
8 | result[key] = value
9 | end
10 |
11 | return result
12 | end
13 |
14 | local ExpectationContext = {}
15 | ExpectationContext.__index = ExpectationContext
16 |
17 | function ExpectationContext.new(parent)
18 | local self = {
19 | _extensions = parent and copy(parent._extensions) or {},
20 | }
21 |
22 | return setmetatable(self, ExpectationContext)
23 | end
24 |
25 | function ExpectationContext:startExpectationChain(...)
26 | return Expectation.new(...):extend(self._extensions)
27 | end
28 |
29 | function ExpectationContext:extend(config)
30 | for key, value in pairs(config) do
31 | assert(self._extensions[key] == nil, string.format("Cannot reassign %q in expect.extend", key))
32 | assert(checkMatcherNameCollisions(key), string.format("Cannot overwrite matcher %q; it already exists", key))
33 |
34 | self._extensions[key] = value
35 | end
36 | end
37 |
38 | return ExpectationContext
39 |
--------------------------------------------------------------------------------
/test/TestEZ/LifecycleHooks.luau:
--------------------------------------------------------------------------------
1 | local TestEnum = require(script.Parent.TestEnum)
2 |
3 | local LifecycleHooks = {}
4 | LifecycleHooks.__index = LifecycleHooks
5 |
6 | function LifecycleHooks.new()
7 | local self = {
8 | _stack = {},
9 | }
10 | return setmetatable(self, LifecycleHooks)
11 | end
12 |
13 | --[[
14 | Returns an array of `beforeEach` hooks in FIFO order
15 | ]]
16 | function LifecycleHooks:getBeforeEachHooks()
17 | local key = TestEnum.NodeType.BeforeEach
18 | local hooks = {}
19 |
20 | for _, level in ipairs(self._stack) do
21 | for _, hook in ipairs(level[key]) do
22 | table.insert(hooks, hook)
23 | end
24 | end
25 |
26 | return hooks
27 | end
28 |
29 | --[[
30 | Returns an array of `afterEach` hooks in FILO order
31 | ]]
32 | function LifecycleHooks:getAfterEachHooks()
33 | local key = TestEnum.NodeType.AfterEach
34 | local hooks = {}
35 |
36 | for _, level in ipairs(self._stack) do
37 | for _, hook in ipairs(level[key]) do
38 | table.insert(hooks, 1, hook)
39 | end
40 | end
41 |
42 | return hooks
43 | end
44 |
45 | --[[
46 | Pushes uncalled beforeAll and afterAll hooks back up the stack
47 | ]]
48 | function LifecycleHooks:popHooks()
49 | table.remove(self._stack, #self._stack)
50 | end
51 |
52 | function LifecycleHooks:pushHooksFrom(planNode)
53 | assert(planNode ~= nil)
54 |
55 | table.insert(self._stack, {
56 | [TestEnum.NodeType.BeforeAll] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.BeforeAll),
57 | [TestEnum.NodeType.AfterAll] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.AfterAll),
58 | [TestEnum.NodeType.BeforeEach] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.BeforeEach),
59 | [TestEnum.NodeType.AfterEach] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.AfterEach),
60 | })
61 | end
62 |
63 | --[[
64 | Get the beforeAll hooks from the current level.
65 | ]]
66 | function LifecycleHooks:getBeforeAllHooks()
67 | return self._stack[#self._stack][TestEnum.NodeType.BeforeAll]
68 | end
69 |
70 | --[[
71 | Get the afterAll hooks from the current level.
72 | ]]
73 | function LifecycleHooks:getAfterAllHooks()
74 | return self._stack[#self._stack][TestEnum.NodeType.AfterAll]
75 | end
76 |
77 | function LifecycleHooks:_getHooksOfType(nodes, key)
78 | local hooks = {}
79 |
80 | for _, node in ipairs(nodes) do
81 | if node.type == key then
82 | table.insert(hooks, node.callback)
83 | end
84 | end
85 |
86 | return hooks
87 | end
88 |
89 | return LifecycleHooks
90 |
--------------------------------------------------------------------------------
/test/TestEZ/Reporters/TextReporterQuiet.luau:
--------------------------------------------------------------------------------
1 | --[[
2 | Copy of TextReporter that doesn't output successful tests.
3 |
4 | This should be temporary, it's just a workaround to make CI environments
5 | happy in the short-term.
6 | ]]
7 |
8 | local TestService = game:GetService("TestService")
9 |
10 | local TestEnum = require(script.Parent.Parent.TestEnum)
11 |
12 | local INDENT = (" "):rep(3)
13 | local STATUS_SYMBOLS = {
14 | [TestEnum.TestStatus.Success] = "+",
15 | [TestEnum.TestStatus.Failure] = "-",
16 | [TestEnum.TestStatus.Skipped] = "~"
17 | }
18 | local UNKNOWN_STATUS_SYMBOL = "?"
19 |
20 | local TextReporterQuiet = {}
21 |
22 | local function reportNode(node, buffer, level)
23 | buffer = buffer or {}
24 | level = level or 0
25 |
26 | if node.status == TestEnum.TestStatus.Skipped then
27 | return buffer
28 | end
29 |
30 | local line
31 |
32 | if node.status ~= TestEnum.TestStatus.Success then
33 | local symbol = STATUS_SYMBOLS[node.status] or UNKNOWN_STATUS_SYMBOL
34 |
35 | line = ("%s[%s] %s"):format(
36 | INDENT:rep(level),
37 | symbol,
38 | node.planNode.phrase
39 | )
40 | end
41 |
42 | table.insert(buffer, line)
43 |
44 | for _, child in ipairs(node.children) do
45 | reportNode(child, buffer, level + 1)
46 | end
47 |
48 | return buffer
49 | end
50 |
51 | local function reportRoot(node)
52 | local buffer = {}
53 |
54 | for _, child in ipairs(node.children) do
55 | reportNode(child, buffer, 0)
56 | end
57 |
58 | return buffer
59 | end
60 |
61 | local function report(root)
62 | local buffer = reportRoot(root)
63 |
64 | return table.concat(buffer, "\n")
65 | end
66 |
67 | function TextReporterQuiet.report(results)
68 | local resultBuffer = {
69 | "Test results:",
70 | report(results),
71 | ("%d passed, %d failed, %d skipped"):format(
72 | results.successCount,
73 | results.failureCount,
74 | results.skippedCount
75 | )
76 | }
77 |
78 | print(table.concat(resultBuffer, "\n"))
79 |
80 | if results.failureCount > 0 then
81 | print(("%d test nodes reported failures."):format(results.failureCount))
82 | end
83 |
84 | if #results.errors > 0 then
85 | print("Errors reported by tests:")
86 | print("")
87 |
88 | for _, message in ipairs(results.errors) do
89 | TestService:Error(message)
90 |
91 | -- Insert a blank line after each error
92 | print("")
93 | end
94 | end
95 | end
96 |
97 | return TextReporterQuiet
--------------------------------------------------------------------------------
/test/TestEZ/TestEnum.luau:
--------------------------------------------------------------------------------
1 | --[[
2 | Constants used throughout the testing framework.
3 | ]]
4 |
5 | local TestEnum = {}
6 |
7 | TestEnum.TestStatus = {
8 | Success = "Success",
9 | Failure = "Failure",
10 | Skipped = "Skipped"
11 | }
12 |
13 | TestEnum.NodeType = {
14 | Describe = "Describe",
15 | It = "It",
16 | BeforeAll = "BeforeAll",
17 | AfterAll = "AfterAll",
18 | BeforeEach = "BeforeEach",
19 | AfterEach = "AfterEach"
20 | }
21 |
22 | TestEnum.NodeModifier = {
23 | None = "None",
24 | Skip = "Skip",
25 | Focus = "Focus"
26 | }
27 |
28 | return TestEnum
--------------------------------------------------------------------------------
/test/TestEZ/TestPlanner.luau:
--------------------------------------------------------------------------------
1 | --[[
2 | Turns a series of specification functions into a test plan.
3 |
4 | Uses a TestPlanBuilder to keep track of the state of the tree being built.
5 | ]]
6 | local TestPlan = require(script.Parent.TestPlan)
7 |
8 | local TestPlanner = {}
9 |
10 | --[[
11 | Create a new TestPlan from a list of specification functions.
12 |
13 | These functions should call a combination of `describe` and `it` (and their
14 | variants), which will be turned into a test plan to be executed.
15 |
16 | Parameters:
17 | - modulesList - list of tables describing test modules {
18 | method, -- specification function described above
19 | path, -- array of parent entires, first element is the leaf that owns `method`
20 | pathStringForSorting -- a string representation of `path`, used for sorting of the test plan
21 | }
22 | - testNamePattern - Only tests matching this Lua pattern string will run. Pass empty or nil to run all tests
23 | - extraEnvironment - Lua table holding additional functions and variables to be injected into the specification
24 | function during execution
25 | ]]
26 | function TestPlanner.createPlan(modulesList, testNamePattern, extraEnvironment)
27 | local plan = TestPlan.new(testNamePattern, extraEnvironment)
28 |
29 | table.sort(modulesList, function(a, b)
30 | return a.pathStringForSorting < b.pathStringForSorting
31 | end)
32 |
33 | for _, module in ipairs(modulesList) do
34 | plan:addRoot(module.path, module.method)
35 | end
36 |
37 | return plan
38 | end
39 |
40 | return TestPlanner
--------------------------------------------------------------------------------
/test/TestEZ/init.luau:
--------------------------------------------------------------------------------
1 | local Expectation = require(script.Expectation)
2 | local TestBootstrap = require(script.TestBootstrap)
3 | local TestEnum = require(script.TestEnum)
4 | local TestPlan = require(script.TestPlan)
5 | local TestPlanner = require(script.TestPlanner)
6 | local TestResults = require(script.TestResults)
7 | local TestRunner = require(script.TestRunner)
8 | local TestSession = require(script.TestSession)
9 | local TextReporter = require(script.Reporters.TextReporter)
10 | local TextReporterQuiet = require(script.Reporters.TextReporterQuiet)
11 | local TeamCityReporter = require(script.Reporters.TeamCityReporter)
12 |
13 | local function run(testRoot, callback)
14 | local modules = TestBootstrap:getModules(testRoot)
15 | local plan = TestPlanner.createPlan(modules)
16 | local results = TestRunner.runPlan(plan)
17 |
18 | callback(results)
19 | end
20 |
21 | local TestEZ = {
22 | run = run,
23 |
24 | Expectation = Expectation,
25 | TestBootstrap = TestBootstrap,
26 | TestEnum = TestEnum,
27 | TestPlan = TestPlan,
28 | TestPlanner = TestPlanner,
29 | TestResults = TestResults,
30 | TestRunner = TestRunner,
31 | TestSession = TestSession,
32 |
33 | Reporters = {
34 | TextReporter = TextReporter,
35 | TextReporterQuiet = TextReporterQuiet,
36 | TeamCityReporter = TeamCityReporter,
37 | },
38 | }
39 |
40 | return TestEZ
--------------------------------------------------------------------------------
/test/TestVars.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | return {
4 | runTests = true
5 | }
--------------------------------------------------------------------------------
/test/Util/FiniteTime.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | local FiniteTime = {}
4 |
5 | local DEFAULT_TIME_LIMIT = 1 / 10
6 |
7 | function FiniteTime.start(
8 | timeLimit: number?
9 | ): () -> ()
10 | local endTime = os.clock() + (timeLimit or DEFAULT_TIME_LIMIT)
11 | local errored = false
12 | local function check()
13 | if os.clock() > endTime and not errored then
14 | errored = true
15 | error("Finite time limit reached. (FUSION_TEST_FINITE_TIME)", 0)
16 | end
17 | end
18 |
19 | return check
20 | end
21 |
22 | return FiniteTime
--------------------------------------------------------------------------------
/test/init.server.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 |
3 | local TestEZ = require(script.TestEZ) :: any
4 | local TestVars = require(script.TestVars)
5 |
6 | local ReplicatedStorage = game:GetService("ReplicatedStorage")
7 | local Fusion = ReplicatedStorage.Fusion
8 |
9 | local External = require(Fusion.External)
10 | local SpecExternal = require(script.SpecExternal)
11 |
12 | -- run unit tests
13 | if TestVars.runTests then
14 | print("Running unit tests...")
15 | External.safetyTimerMultiplier = 1 / 20
16 | External.setExternalProvider(SpecExternal)
17 |
18 | local data
19 | SpecExternal.doTaskDeferred(function()
20 | data = (TestEZ.TestBootstrap :: any):run({script.Spec})
21 | end)
22 | SpecExternal.step(0)
23 |
24 | if data == nil or data.failureCount > 0 then
25 | return
26 | end
27 | end
--------------------------------------------------------------------------------
/wally.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "elttob/fusion"
3 | description = "A modern reactive UI library, built specifically for Roblox and Luau"
4 | license = "MIT"
5 | authors = ["Elttob"]
6 | version = "0.4.0-dev1"
7 |
8 | registry = "https://github.com/UpliftGames/wally-index"
9 | realm = "shared"
10 |
11 | exclude = ["**"]
12 | include = ["src", "src/*", "default.project.json", "wally.lock", "wally.toml"]
13 |
14 | [dependencies]
15 |
--------------------------------------------------------------------------------