├── .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 | FusionFusionDocsDownload 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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | Tween 10 | 11 | -> Tween<T> 12 | 13 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | Animatable 10 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | Spring 10 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | Tween 10 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | Contextual 10 | 11 | -> Contextual<T> 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | Safe 10 | 11 | -> Success | Fail 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | version 10 | 11 | : Version 12 | 13 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | Contextual 10 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | Version 10 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | Observer 10 | 11 | -> Observer 12 | 13 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | Observer 10 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | deriveScope 10 | 11 | -> Scope<T> 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | doCleanup 10 | 11 | -> () 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | innerScope 10 | 11 | -> Scope<T> 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | insert 10 | 11 | -> Tasks... 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | scoped 10 | 11 | -> Scope<T> 12 | 13 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | Scope 10 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | ScopedObject 10 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | Task 10 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | Attribute 10 | 11 | -> SpecialKey 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | AttributeChange 10 | 11 | -> SpecialKey 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | AttributeOut 10 | 11 | -> SpecialKey 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | Child 10 | 11 | -> Child 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | Children 10 | 11 | : SpecialKey 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | Hydrate 10 | 11 | -> (PropertyTable) -> Instance 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | New 10 | 11 | -> (PropertyTable) -> Instance 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | OnChange 10 | 11 | -> SpecialKey 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | OnEvent 10 | 11 | -> SpecialKey 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | Out 10 | 11 | -> SpecialKey 12 | 13 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | Child 10 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | PropertyTable 10 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | Computed 10 | 11 | -> Computed<T> 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | peek 10 | 11 | : Use 12 | 13 |

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 | 6 | 7 |

8 | :octicons-workflow-24: 9 | Value 10 | 11 | -> Value<T> 12 | 13 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | Computed 10 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | For 10 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | StateObject 10 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | Use 10 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | UsedAs 10 |

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 | 6 | 7 |

8 | :octicons-note-24: 9 | Value 10 |

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 | logo 4 | logo 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 | ![A photo taken in Fusion Wordle, showing a completed game board](place-thumbnails/Fusion-Wordle.jpg) 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 | ![A photo taken in Fusion Obby, showing the counter and confetti](place-thumbnails/Fusion-Obby.jpg) 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 | ![Showing the difference between a text label made with Instance.new and Fusion's New function.](Default-Props-Dark.svg#only-dark) 65 | ![Showing the difference between a text label made with Instance.new and Fusion's New function.](Default-Props-Light.svg#only-light) 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 | --------------------------------------------------------------------------------