GithubHelp home page GithubHelp logo

elttob / fusion Goto Github PK

View Code? Open in Web Editor NEW
487.0 20.0 86.0 51.42 MB

A modern reactive UI library, built specifically for Roblox and Luau.

Home Page: https://elttob.uk/Fusion/

License: MIT License

Lua 100.00%
roblox ui luau reactive declarative animation state-management

fusion's Introduction

FusionFusionDocsDownload

Rediscover the joy of coding.

Code is more dynamic, complex and intertwined than ever before. Errors cascade out of control, things update in the wrong order, and it's all connected by difficult, unreadable spaghetti.

No longer. Fusion introduces modern 'reactive' concepts for managing code, so you can spend more time getting your logic right, and less time implementing buggy boilerplate code connections.

Starting from simple roots, concepts neatly combine and build up with very little learning curve. At every stage, you can robustly guarantee what your code will do, and when you come back in six months, your code is easy to pick back up.

Piqued your interest? Get going in minutes with our on-rails tutorial.

Issues & contributions

Have you read our contribution guide? It's a real page turner!

We highly recommend reading it before opening an issue or pull request.

License

Fusion is licensed freely under MIT. Go do cool stuff with it, and if you feel like it, give us a shoutout!

fusion's People

Contributors

4812571 avatar aimarekin avatar alexasterisk avatar almost89 avatar aloroid avatar anaminus avatar astrealrblx avatar blake-mealey avatar boatbomber avatar chipioindustries avatar dimitarbogdanov avatar dionysusnu avatar dphfox avatar eqicness avatar gargafield avatar krypt102 avatar metatablecat avatar nottirb avatar nullwoof avatar quantix-dev avatar quenty avatar raild3x avatar railworks2 avatar raphtalia avatar syrilai avatar ukendio avatar xswezan avatar zayerrblx avatar zw1ce avatar zyrakia avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fusion's Issues

Springs and Tweens should have modifiable parameters after construction

I already have setter-based methods for doing this, however I'm not sure if they make sense with the rest of Fusion's API design, which works with state objects.

I currently have these setters disabled behind flags for this reason (ENABLE_PARAM_SETTERS), while I investigate these API designs further. This shouldn't affect the basic functionality of spring and tween object though.

Spring simulate 3D rotations more naturally

Currently, to animate the rotational part of a CFrame using spring simulation, we decompose the CFrame into a series of numbers representing it's axis-angle rotation, and spring simulate those numbers. While this works, it doesn't seem like a nice or natural way of doing it, especially since the axis needs to be unitised before being re-assembled into CFrame form.

Is there a better way of doing this? I'm still learning to work my way around quaternions, but I think they'd need to be orthonormalised too. Perhaps the real problem is that we need a new kind of spring for rotations and angles?

[Cleanup] - specify destruction tasks for the New function

Right now in Fusion, there's no way to disconnect event connections or run cleanup code when an instance is destroyed or garbage collected. However, Fusion already has mechanisms internally to check for this and run internal cleanup. This could be useful to expose to users to allow for easier and more proper cleanup.

Cleanup would be a Maid-like API, accepting the following values:

  • instances - get destroyed
  • event connections - get disconnected
  • functions - get called
  • objects - get :destroy() or :Destroy() called
  • arrays - all array contents are cleaned up

Example usage:

local conn = RunService.RenderStepped:Connect(...)

local ins = New "Folder" {
    [Cleanup] = {conn}
} 

ins:Destroy() -- disconnects 'conn' 

ComputedPairs leaks memory

Reported and repro'd by @/boatbomber

ComputedPairs currently has an undiagnosed memory leak - possibly may be related to automatic dependency management?

'New' gives misleading error message when a property exists but is of invalid type

This code snippet:

New "TextLabel" {
	Position = UDim.new()
}

Produces the following error:

[Fusion] The class type 'TextLabel' has no assignable property 'Position'. (ID: cannotAssignProperty)

This took quite a while to track down since Position is indeed an assignable property of TextLabel, it was just given the wrong type of value for that property because of a typo.

Deep equality checking

Currently, Fusion uses referential equality - essentially, just ==. However, this can lead to a lot of footguns, like this:

local array = {1, 2, 3, 4, 5}
local state = State(array)

table.insert(array, 6)
state:set(array) -- doesn't update, because the reference hasn't changed
local state = State({ message = "Hello" })

local computed = Computed(function()
    return state:get().message
end)

state:set({ message = "Hello" }) -- updates `computed` even though the message is identical, because the reference changed

Should Fusion also support deep equality checking? This would almost certainly have a performance impact, though I haven't prototyped and benchmarked anything yet.

[Tag] for CollectionService tags

Add a [Tags] symbol to support construction of instances with tags. Similar to [Children], except the elements are just strings:

-- Single tag construction.
New "Folder" {
	[Tags] = "Foo",
}

-- Multiple tag construction.
New "Folder" {
	[Tags] = {"Foo", "Bar"},
}

-- Nested tag construction.
New "Folder" {
	[Tags] = {"Foo", "Bar", {"Baz"}},
}

Support for states?

local tag = State("Foo")

local folder = New "Folder" {
	[Tags] = tag,
}

tag:set("Bar") -- Adds "Bar", removes "Foo".
local tagA = State("Foo")
local tagB = State("Foo")

local folder = New "Folder" {
	[Tags] = {tagA, tagB},
}

tagA:set("Bar") -- Adds "Bar", "Foo" still retained by tagB.
tagB:set("Bar") -- Adds "Bar", removes "Foo".
local tags = State({"Foo", "Bar"})

local folder = New "Folder" {
	[Tags] = tags,
}

tags:set({"Bar", "Baz"}) -- Removes "Foo", adds "Baz".
tags:set("Foo") -- Removes "Bar" and "Baz", adds "Foo".

This could make use of #10 under the hood if that is implemented. Alternatively, it could detect keys instead of values, but seems somewhat less convenient.

Another consideration is whether [Tags] represents all tags on the instance, or only the tags known by bound states.

local tags = State({"Foo", "Bar"})

local folder = New "Folder" {
	[Tags] = tags,
}

CollectionService:AddTag(folder, "Baz")
tags:set({"Bar"}) -- Removes "Foo", should this remove "Baz"?

[Children] has a similar consideration, so [Tags] should match whatever that does.

Remove .onChange events for state objects

Currently, all state objects (State/Computed/ComputedPairs/Tween/Spring) provide an .onChange event, so that user code can detect and respond to changes in a state object's value, without having to implement a custom object to hook into the reactive graph.

However, there's two large problems with events as they currently stand:

  • if they're not disconnected after use, they can cause massive leaks and force entire reactive graphs to be held in memory
  • they seem to lead to anti-patterns in codebases where they're used; for example, daisy-chaining together state objects using onChange, rather than using computed objects (which can be properly accelerated by Fusion).

The first problem may be technically possible to solve, but it would likely involve adding a significant level of overhead. The second problem is much more serious and might be better to avoid entirely.

The original motivation for adding these events is for compatibility: if an end user is attempting to embed a non-Fusion UI inside a Fusion UI, it would be useful to have an imperative 'escape hatch' so they can communicate changes in state to their non-Fusion UI.

I'm not sure what the correct solution to this problem is, but I don't think events work here. I've already been working to remove all uses of events internally in Fusion in favour of using the reactive graph, so this wouldn't require any large refactors of library code to remove.

Allow NumberValues, StringValues, IntValues, etc to be passed into State objects and read as their respective properties by default

I ran into this issue with a UI I tried converting from raw Roblox Instances to Fusion code.

It would be nice if when any of these:
Picture of Instances that store binary data

were passed into a State, it would automatically create a .Changed event listener and whenever :get() was called on the State, it would return the value. Furthermore, Computed() would automatically update whenever the value of the object was changed. If this was added, instead of having to do this:

local Object = Player.leaderstats.Money

-- Instead of this...

local Money = State(Object.Value)

Object.Changed:Connect(function(x)
	Money:set(x)
end)

New "TextLabel" {
	Parent = Player.PlayerGui,
	Text = Computed(function()
		return Money:get()
	end)
}

-- I would be able to do this...


local Money = State(Object) -- You could add an optional "bypassValueObject" parameter if for some reason people wanted to throw the raw instance into the state

New "TextLabel" {
	Parent = Player.PlayerGui,
	Text = Computed(function()
		return Money:get()
	end)
}

Animation timelines/sequences/keyframes

Right now, it can be awkward to work with animations in Fusion that involve multiple keyframes or values.

Fusion could implement a Timeline object which allows the user to define 'keyframes' for a value, which can then be moved/interpolated between by some state representing the current time:

local currentTime = State(0)

local colour = Timeline(currentTime, {
  [0] = Color3.new(1, 0, 0)
  [0.5] = Color3.new(0, 1, 0)
  [1] = Color3.new(0, 0, 1)
})

print(colour:get()) -- red

currentTime:set(0.5)
print(colour:get()) -- green

currentTime:set(0.75)
print(colour:get()) -- halfway between green and blue

currentTime:set(1)
print(colour:get()) --blue

This would be useful for defining more complex declarative animations in Fusion.

Render throttling/pausing

In some contexts such as plugin development, it may be desirable to throttle or pause render step actions (e.g. stepping animations) to reduce background resource usage e.g. when a plugin widget is not enabled.

It may be desirable to introduce tools for limiting or pausing animation, state and UI updates either locally or globally to allow users to conserve resources easily.

Attempting to set a property using State to an incompatible type throws a misleading error.

Example:

New "TextLabel" {
     Text = slotStates["Quantity"];
};

If the value of slotStates["Quantity"]:get() is nil, this error is thrown:

[Fusion] The class type 'TextLabel' has no assignable property 'Text'. (ID: cannotAssignProperty)

When, in reality, TextLabel's do have an assignable property 'Text'. There should be a separate error message for incompatible type assignments.

State freezing

It could be an interesting idea to consider a state object that can 'freeze' its value.

This hypothetical code snippet demonstrates the basic principle. While the value is not frozen, the object acts like a transparent 'pass-through' to the source state. When the value is frozen, the object retains the last value it had indefinitely, ignoring all changes made.

local source = State(5)
local isFrozen = State(false)

local output = Freeze(source, isFrozen)

print(source:get(), output:get()) --> 5 5

source:set(15)
print(source:get(), output:get()) --> 15 15

isFrozen:set(true)
source:set(2)
print(source:get(), output:get()) --> 2 15

isFrozen:set(false)
print(source:get(), output:get()) --> 2 2

You could implement this using a computed, and a bit of impurity and use of undocumented API parameters:

local function Freeze(source, isFrozen)
    local lastValue = source:get(false)
    return Computed(function()
        if not isFrozen:get() then
            lastValue = source:get()
        end
        return lastValue
    end)
end

However, I think this may find some important applications in optimisation - specifically, you can freeze inputs to costly processes if their output would not be visible, for example freezing a list's contents while it's not visible, or freezing inputs to a procedural hover animation while the cursor is not over the element. It may be valuable enough to include as part of Fusion's standard tools.

Velocity(speedState) animation helper state-like

This is currently not great to do, usually having to update another state on heartbeat, incrementing it with the current speed.
This helper would wrap that behaviour.
It could also have wrap-around behaviour where it loops around a number range like an integer overflowing. This would for example be useful for an infinitely rotating loading circle.
Adding is probably subject to #40

Should Fusion introduce a Roact-like form of the New function?

It was suggested in a recent PR that we could consider adding a more Roact-like form of the New function, accepting all arguments in one call rather than as a curried function. While I rejected the PR on the grounds that changes should be discussed before they're added, I'm opening an issue to discuss it here :)

Hypothetical usage:

-- option 1 for children - just like we do now:
local ui = New("Frame", {
    Name = "Foo",
    Visible = true,

    [Children] = New("UICorner", {...})
})

-- option 2 for children - separate it like Roact
local ui = New("Frame", {
    Name = "Foo",
    Visible = true
}, {
    New("UICorner", {...})
})

Sensible defaults for New

By default, Fusion doesn't apply any defaults to instances you create with the New function, meaning any instances created will have Roblox's default properties applied. However, these are rarely useful and have often caused subtle bugs with font, text and borders appearing incorrectly due to these defaults, even in official Roblox UI.

Some specific pain points:

  • layer collectors default to ResetOnSpawn, which is nowadays not typically the desired behaviour
  • layer collectors default to Global ZIndexBehavior, which often leads to poorly managed UI and nonlocal depth sorting issues
  • surface gui objects default to 'fixed size' scaling, which can lead to non-uniform scaling
  • gui objects' backgrounds default to an unflattering shade of grey, which nobody wants to use
  • gui objects come with 1px 'classic' borders by default, which are hard to spot if left unremoved in dark UIs - classic borders should be opt-in as they are not widely used, and likely will be deprecated in favour of UIStroke
  • text objects default to 8px Legacy font which is deprecated, less accessible and generally looks terrible
  • text objects default to having 'Label' or 'Button' text which is not useful, and can be hard to spot if left unremoved in dark UIs - default properties should not include content as this is up to the developer to provide
  • button objects default to having automatic button colouring enabled - this is typically behaviour that's customised by the developer, so should be opt-in
  • text boxes default ClearTextOnFocus to true, which is counterintuitive behaviour which is detrimental to UX, and almost always turned off manually

To address these points, I plan to introduce the following 'sensible default' properties to instances created by the New function:

ScreenGui = {
	ResetOnSpawn = false,
	ZIndexBehavior = "Sibling"
},

BillboardGui = {
	ResetOnSpawn = false,
	ZIndexBehavior = "Sibling"
},

SurfaceGui = {
	ResetOnSpawn = false,
	ZIndexBehavior = "Sibling",

	SizingMode = "PixelsPerStud",
	PixelsPerStud = 50
},

Frame = {
	BackgroundColor3 = Color3.new(1, 1, 1),
	BorderColor3 = Color3.new(0, 0, 0),
	BorderSizePixel = 0
},

ScrollingFrame = {
	BackgroundColor3 = Color3.new(1, 1, 1),
	BorderColor3 = Color3.new(0, 0, 0),
	BorderSizePixel = 0,

	ScrollBarImageColor3 = Color3.new(0, 0, 0)
},

TextLabel = {
	BackgroundColor3 = Color3.new(1, 1, 1),
	BorderColor3 = Color3.new(0, 0, 0),
	BorderSizePixel = 0,

	Font = "SourceSans",
	Text = "",
	TextColor3 = Color3.new(0, 0, 0),
	TextSize = 14
},

TextButton = {
	BackgroundColor3 = Color3.new(1, 1, 1),
	BorderColor3 = Color3.new(0, 0, 0),
	BorderSizePixel = 0,

	AutoButtonColor = false,

	Font = "SourceSans",
	Text = "",
	TextColor3 = Color3.new(0, 0, 0),
	TextSize = 14
},

TextBox = {
	BackgroundColor3 = Color3.new(1, 1, 1),
	BorderColor3 = Color3.new(0, 0, 0),
	BorderSizePixel = 0,

	ClearTextOnFocus = false,

	Font = "SourceSans",
	Text = "",
	TextColor3 = Color3.new(0, 0, 0),
	TextSize = 14
},

ImageLabel = {
	BackgroundColor3 = Color3.new(1, 1, 1),
	BorderColor3 = Color3.new(0, 0, 0),
	BorderSizePixel = 0
},

ImageButton = {
	BackgroundColor3 = Color3.new(1, 1, 1),
	BorderColor3 = Color3.new(0, 0, 0),
	BorderSizePixel = 0,

	AutoButtonColor = false
},

ViewportFrame = {
	BackgroundColor3 = Color3.new(1, 1, 1),
	BorderColor3 = Color3.new(0, 0, 0),
	BorderSizePixel = 0
},

VideoFrame = {
	BackgroundColor3 = Color3.new(1, 1, 1),
	BorderColor3 = Color3.new(0, 0, 0),
	BorderSizePixel = 0
}

Are these agreeable defaults? They don't have to (and realistically can't) reflect the choices that everyone would personally make, but should be a better baseline to start from for everyone by removing obstacles and pitfalls we all have to deal with.

I'd like to get this decided before Fusion releases to a wider audience, since this is technically a breaking change - some UIs could end up depending on the old defaults.

Should we maintain an official model in the toolbox?

Right now, we only distribute Fusion here on Github. With the recent announcement of the Open Cloud APIs, would it be worth setting up some kind of action to auto-publish a Fusion model to the Roblox toolbox?

This would help make Fusion much more immediately accessible for all developers, could make the installation process less intimidating for newer developers, and may even allow for require by asset ID.

With regards to require by asset ID, this would allow games to have their Fusion versions updated over the air automatically - we should consider this carefully. Since Fusion will use semver, this would probably result in one model per major version, with minor non-breaking updates being automatically patched in. If we do this, we need to guarantee to the best of our ability that these updates aren't breaking, though this is something we should do anyway.

I'd be interested to hear what your takes are on such a proposition!

Create SECURITY.md

I found a potential secret leaked within the repository, please create a SECURITY.md file so I can report this.

New/cleanupOnDestroy(?) leaks memory

In the process of fixing a memory leak with @/boatbomber, we discovered there's a separate memory leak in Fusion which doesn't appear to be related to the reactive graph. (thank goodness, I didn't want to rewrite another state object ;p)

My current hypothesis is that there's a leak somewhere in the New function (perhaps cleanupOnDestroy?) but until I have concrete proof of this I'll leave this issue with an open title.

Treating constants like state objects

As mentioned in this Twitter thread (https://twitter.com/Elttob_/status/1432242081368530949):

[The downside of state objects] is that, when dealing with state objects, you still need to make a case for constants. In some Fusion prototypes I experimented with a Statify() or makeState() function to help with this. If you give it a state object, it just returns that state object. Otherwise, it'll wrap the value in a state-like API. This means you can write your code once for state objects, then just pass your inputs through makeState() to have it work for constant inputs too.

Hypothetical usage:

local function printValue(x)
    x = makeState(x)
    print(x:get())
end

local constant = "I am constant"
local state = State("I am state")

printValue(constant) --> I am constant
printValue(state) --> I am state

Support for function and class keys in New function.

Right now there's a lot of requests to add different things to the New function (#36 #19 #1). So I propose the support for functions as keys in the New function. This allows for more modular support.

Here's how it could work:

-- Define our function
-- inst is the instance that New creates
-- props are the value of that key.
function Tag(inst, props)
    if type(props) == "table" then
        for _, tag in ipairs(props) do
            CollectionService:AddTag(inst, tag)
        end
    else
        CollectionService:AddTag(inst, props)
    end
end

-- Using it
local part = New("Part")({
    Name = "Part",
    Parent = workspace,
    Position = Vector3.new(0,0,0),
    [Tag] = {"Part", "Idk"}
})

[Ref] to get a reference to an instance from New

Right now in Fusion, the New function returns the instance it constructs. This is how you currently get a reference to the instance:

local textLabel = New "TextLabel" {
    Name = "Bob"
}

print(typeof(textLabel)) -- Instance
print(textLabel.Name) --Bob

While this works well enough for shallow trees, it can be difficult and unergonomic to get references to children nested inside other New calls, since the return value is the only way to get a reference:

local deeplyNestedChild = New "TextLabel" {
    Name = "Foo"
}

local root = New "Folder" {
    [Children] = New "Folder" {
        [Children] = deeplyNestedChild
    }
}

The TextLabel has to be separated from the main hierachy of New calls, which makes it less clear where it sits in the hierachy.

To solve this, we could introduce a [Ref] symbol which can be used to save an instance reference into an existing State object. This was experimented with in prototypes before, but was ultimately dropped from the initial beta:

local ref = State()

local root = New "Folder" {
    [Children] = New "Folder" {
        -- deeply nested child can remain inline
        [Children] = New "TextLabel" {
            [Ref] = ref,
            Name = "Foo"
        }
    }
}

print(typeof(ref:get())) -- Instance
print(ref:get().Name) --Bob

The State object would be :set() with the instance by the New function upon running.

Some open questions:

  • should we unset the state object on destroy?
  • what would this imply for garbage collection in different scenarios?
  • what other solutions to this problem exist - are there any more elegant solutions that can take advantage of the return value?

Bad error returned with New

When we create a new Instance using New and we set a property with a boolean state, an error is returned saying that the instance does not have this property.

However, when we transform our 'state' into a string using tostring, it works.

local myState = State(true)

New 'TextLabel' {
    -- Working
    Text = Computed(function()
        return tostring(myState:get())
    end)
    -- Working
    Text = tostring(myState:get())
    -- Return [Fusion] The class type 'TextLabel' has no assignable property 'Text'. (ID: cannotAssignProperty)
    Text = myState:get()
},

The wrong error is therefore sent.

Fusion internal formatting

There are currently a few formatting decisions in Fusion, that are not widely used, for a few reasons:

  • No trailing commas in multi-line tables, and no newlines at end of files
    • This hurts commit diffs, because adding new code will modify the previous line
  • Empty blocks or single statement blocks being multi-line
    • Things like if condition then return end and function() end
    • This is usually just as clean when put on single lines, although admittedly writer preference.
  • No whitespace between table brace and items
    • This hurts readability by making tables seem smushed together.
    • This is currently not consistent in the codebase, with only some parts using whitespace.
  • Operators are often placed after their left expressions, when their right expression is on the next line
    • This harms readability and looks confusing.
  • There are unnecessary empty lines at the starts or ends of blocks
    • As the existing style guide says, this only makes the file harder to read.
  • Operators are mixed in whitespace usage, in order to signify precendence
    • Although this helps with detailed looks at math, it harms readability because of the code being too compact.

ComputedSet (values-only ComputedPairs)

Right now, developers using ComputedPairs have to be careful not to use unstable keys. This can subtly cause extra recalculations:

local data = State({"Red", "Green", "Blue", "Yellow"})

print("Creating processedData...")

local processedData = ComputedPairs(function(key, value)
    print("  ...recalculating key: " .. key .. " value: " .. value)
    return value
end)

print("Removing Blue...")
data:set({"Red", "Green", "Yellow"})

UnstableKeys-bg

Right now, the solution to this problem is to make the keys stable, usually by using the values as keys:

local data = State({Red = true, Green = true, Blue = true, Yellow = true})

print("Creating processedData...")

local processedData = ComputedPairs(function(key)
    print("  ...recalculating key: " .. key)
    return key
end)

print("Removing Blue...")
data:set({Red = true, Green = true, Yellow = true})

StableKeys-bg

(see https://elttob.github.io/Fusion/tutorials/further-basics/arrays-and-lists/#optimisation)

An alternate solution could be to introduce a ComputedSet object (name open to bikeshedding) - this would act like ComputedPairs, but would ignore keys completely and cache values instead. The values would still have to be non-nil and unique however.

Hypothetical usage:

local data = State({"Red", "Green", "Blue", "Yellow"})

print("Creating processedData...")

local processedData = ComputedSet(function(value)
    print("  ...recalculating value: " .. value)
    return value
end)

print("Removing Blue...")
data:set({"Red", "Green", "Yellow"})

Should OnChange inhibit deferred updating?

Currently, when binding state to a property, changes are deferred until the next render step. Also, when listening for property changes with OnChange, we connect to GetPropertyChangeSignal on the instance.

The combination of these two things is that OnChange only fires on the next render step after the state changes, not immediately when the state object actually changes.

As mentioned in the docs, this can lead to subtle off-by-one-frame errors that are non-obvious. To demonstrate with a (pretty unrealistic) example:

local originalState = State(0)
local syncedState = State(0)

local instance1 = New "NumberValue" {
    Value = originalState,
    [OnChange "Value"] = function(newValue)
        syncedState:set(newValue)
    end
}

local instance2 = New "NumberValue" {
    -- problem: because `syncedState` is set on the render step after `originalState` is set,
    -- this change will be deferred another render step, meaning this instance will lag
    -- behind by one frame
    Value = syncedState
}

When not using OnChange (or OnEvent "Changed" for that matter), deferred updating is almost certainly desirable. However, when using OnChange, it may not always be desirable for this reason.

Should deferred updating be disabled for properties which have OnChange listeners/instances with Changed listeners?

Points to consider:

  • Fusion only knows about event and property change handlers it connects via New - it's impossible to know if other connections exist, which could lead to unexpected behaviour
  • Deferred updating is done to improve performance; by disabling it, would we be introducing notable unnecessary overhead?

I'm currently leaning towards 'no', but it'd be interesting to hear other people's thoughts on this anyway.

Internationalise Fusion

Recently we got a great internationalisation PR #62 which aimed to localise our documentation for Spanish users. I'm already fully on board with these efforts, hence why this issue has skipped evaluating status, but it's not something I'm currently looking to pursue further until the docs are much more solid and not prone to large, sweeping changes.

More generally though, it could be a great idea to have a more holistic internationalisation effort across the entire Fusion project - for example, by introducing localised error messages. If there's any areas of Fusion where you think something more could be done to support your locale, mention it here and I'll make sure it gets accounted for once we start getting to stable releases of Fusion :)

New holds a strong reference to instances created with events/property change handlers/bound state if they're not explicitly Destroyed

I was originally planning to solve this issue pre-initial release, but my initial assumptions about it were false. Luckily, as long as end users always explicitly destroy their instances when they're done with them, this is a non-issue.

I already tried implementing an experimental solution to get gc to work correctly with New (toggled by ENABLE_EXPERIMENTAL_GC_MODE) however this causes some instances to get collected too early. I'm planning to simply redo a lot of this code at some point since it's gotten a bit messy as the requirements have piled on.

I'll return to this issue when I'm not running on low sleep lol.

Event connections should pass the sending instance.

It's currently impossible to reference the Instance in an event connection in an egronomic way.

Sure, we can do the following, but this just feels wrong

local TextButton
TextButton = New "TextButton" {
	Position = UDim2.fromScale(.5, .5),
	AnchorPoint = Vector2.new(.5, .5),
	Size = UDim2.fromOffset(200, 50),

	Text = "Fusion is fun :)",

	[OnEvent "Activated"] = function(...)
		print("Clicked!", TextButton.Name, ...)
	end
}

This syntax will also not work if the event connection is passed as a callback in props.

Roact solved this issue by passing the sender in the event (commonly called rbx)

[Roact.Event.Touched] = function(rbx, touchingPart)

end

It may be possible to combine the two state functions, get and set. But I still feel there may be a few unhandled edgecases.

Compat should not rely on function identity

Compat currently relies on function keys in it's _changeListeners table to store callbacks to be invoked on update. This may become a source of bugs as function identity is no longer guaranteed in modern Luau.

Centralise RunService connections

Right now, multiple parts of the Fusion codebase connect to RunService independently of each other. While this is fine for simplicity, this limits how Fusion can be used - in particular, Fusion can’t be easily adapted to run on the server, as multiple parts of the codebase need to be changed. In addition, we can’t throttle updates - see #5.

It could be a good idea to centralise all RunService connections, so we only have one place where we e.g. connect to RenderStepped, and then just delegate to each part of Fusion that needs to run code on those events. This means we can easily move all the code to another event (like Heartbeat on the server) or even allow the user to manage updates manually, for example only updating Fusion while a plugin widget is visible.

Fusion.Unpack

Right now you have probably 5 fusion imports in every component. This adds to the overall file size and sometimes can be a little irritating keeping track of.
So I propose Fusion.Unpack. It's just a function that returns a tuple with all fusion basics. Here's what it looks like:

local New, State, Computed, Spring = Fusion.Unpack()

And this is how you currently would do this:

local New = Fusion.New
local State = Fusion.State
local Computed = Fusion.Computed
local Spring = Fusion.Spring
-- Or this
local New, State, Computed, Spring = Fusion.New, Fusion.State, Fusion.Computed, Fusion.Spring

Implement native property overriding

One of the biggest pet peeves I have with other UI libraries is overriding native properties. This is a very tedious process when creating components. If I have a text button component having to define each and every single prop that I want to use for native Roblox properties is an absolute pain. There should be some way of replacing the properties of elements in a component easily.

-- TextButton.lua
type Props = {
  Size: UDim2,
  Text: string,
  BackgroundColor3: Color3,
  -- and so on
  
  OnActivated: (input: InputObject, count: number) -> ()
}

return function(props: Props)
  return New 'TextButton' {
    Size = props.Size,
    Text = props.Text,
    BackgroundColor3 = props.BackgroundColor3,
    -- this is tedious work even if a simple loop solves it
    
    [OnEvent 'Activated'] = props.OnActivated
  }
end

-- Container.lua
New 'Frame' {
  Size = UDim2.fromScale(1, 1),
  
  [Children] = {
    TextButton {
      Size = UDim2.new(1, 0, 0, 30),
      -- etc.
    }
  }
}

There's a few possible solutions I can think of but the easiest is probably just some sort of NativeProps() function that will wrap your props in some object that is then found and used when the instance is created by the New constructor.

return New 'TextButton' {
  Text = 'Hello world!',
  Size = UDim2.fromScale(1, 1),
  
  NativeProps(props), -- Should override Text & Size as well as any other native Roblox properties passed but not BackgroundColor3
  
  BackgroundColor3 = Color3.new(0, 0.5, 0)
}

or perhaps since Fusion already uses keys for a variety of things have NativeProps be a special key:

return New 'TextButton' {
  [NativeProps] = props
}

Ability to delay state changes

For an upcoming project, I want all of my components to be on the same Spring but with a different delay so they don't all show up at the exact same time. I'm trying to make it similar to what Super Mario Maker 2 does in Network Play when revealing who you're playing with.

Upload.from.GitHub.for.iOS.MOV

Perhaps this could be implemented with an optional delay argument on the Spring:get() method?

Repo refactors for v0.3

I've heard some people here express concerns about namespace pollution, particularly when talking about Fusion providing utilities that some projects may want to keep a custom implementation of.

One of my visions for how Fusion would work is that it'd be largely complete out of the box, providing everything you need for building most kinds of UI. This would remove the need to search for, choose and download a bunch of libraries just to get started, and help make different Fusion projects more consistent and understandable by sharing a lot of widely used and understood APIs.

I acknowledge however this may have some side effects that could be undesirable:

  • More modules included in Fusion by default means larger file sizes
  • These utilities sharing a namespace could introduce name collision problems (though this can be avoided at the API design stage)
  • Extra unneeded code for projects that replace some utilities

In addition, the extension story for Fusion is rather unclear:

  • How does a third party library know where Fusion is located?
  • How do these libraries extend Fusion when many core APIs for reactive updates and dependency management are not exposed?
  • What standard interfaces should there be between libraries, for example for state objects?

Given these points, it might be worth moving Fusion to a more modular design. Here's a rough idea of how that could work:

  • The official Fusion codebase is split into multiple modules, which can be freely added and removed by end users:
    • Core for fundamental state management (e.g. reactive graph updating, dependency management, cleanup utils, State + Computed)
    • State for extended state tools and utilities (e.g. ComputedPairs, makeState)
    • Instances for Roblox instance APIs (e.g. New, Scheduler, Hydrate)
    • Motion for animation and motion design tools (e.g. Tween, Spring, Keyframes, Timer, Oklab)
  • These modules are contained in ModuleScripts which are parented directly under the main Fusion module. This means that these modules always know where your Fusion module is (and also enables possibly using multiple Fusion installations side by side, which would be particularly useful in dev environments)
  • The default Fusion package includes all modules by default, so people starting out or prototyping with Fusion get the same DX as they do today. However, interested users can remove modules they don't use or replace modules with their own implementation.
  • Other developers making their own third party libraries can leverage this module system to make libraries that can be easily 'plugged into' any Fusion installation.
  • These modules would be required and exposed through the main Fusion module, though the exact process by which this should happen is unclear. Ideally, we should optimise for these use cases at least:
    • Some people use Fusion 'fully qualified'; we don't recommend this because it's especially sensitive to changes like this. e.g. local ui = Fusion.New(...)
    • Code right now expects to find Fusion APIs directly under the main Fusion module
    • Different modules may have name collisions - this is something that should be intentionally avoided for official modules anyway, but third party libraries complicate this

Is this worth pursuing?

Introduce better support for one-shot/impulse/asymmetric animations

Currently we have fantastic utilities for 'symmetric' animations, such as tweens and springs, which always animate the same way regardless of their destination or direction. However, some animations only happen towards certain destinations, animate differently in one direction compared to another, or are invoked at a point of time like an 'impulse' disturbing the animation from a rest state.

We currently don't have any good answers for this kind of animation - it's certainly possible, but it's not easy to do. We're starting to explore this space with Timers (see #12) but I suspect there might be a way of generalising almost all animation of this variety under a general and flexible API design.

I don't have a good idea of what such an API would look like yet.

Timers

A useful addition to Fusion may be an object that acts like a stopwatch - useful for driving animations with especially.

One possible API (using methods to start and pause):

local timer = Timer()

print(timer:get()) -- 0
timer:start()
wait(5)
timer:pause()
print(timer:get()) -- about 5

Another possible API (using a state object to pause and resume)

local paused = State(true)
local timer = Timer(paused)

print(timer:get()) -- 0
paused:set(false)
wait(5)
paused:set(true)
print(timer:get()) -- about 5

There could possibly also be options for setting a 'max duration', looping, variable speed, and setting the current timer position.

Better support for async/promises

It could be nice for Fusion to better support promises as part of its core functionality - for example, being able to conditionally render a loading UI, result UI or error UI based on whether a promise is unresolved, resolved or rejected.

I've only personally used promises outside of Roblox, and so don't yet have enough experience with promises in Lua to reason about what API designs would be most suitable for better supporting them in Fusion (if any supporting API is needed).

should Fusion.New take numeric key children?

Should the following be recognised as a child? Currently it is ignored and throws a warning, unrecognisedPropertyKey

    return New "TextButton" {
        AnchorPoint = Vector2.new(0.5, 0.5),

        BackgroundColor3 = Props.Color,
        Position = Props.Position,
        Size = Props.Size,
        Text = Props.Text,

        New "UICorner" {
            CornerRadius = UDim.new(0, 4)
        }
    }

Binding to existing instances

New allows binding State and whatnot to an instance, but the instance must be created from scratch. There are a handful of useful instances that either already exist, or must be constructed by means other than Instance.new. Such instances should be supported by Fusion. Some off the top of my head:

  • DockWidgetPluginGui
  • PluginAction
  • PluginMenu
  • PluginToolbar
  • PluginToolbarButton
  • Services (probably niche)

One option is to let New (or an additional New-like function) receive an instance instead of a class name. e.g.

New (instance) {
	...
}

Something to consider is passing in instances already known by Fusion. I'm not yet familiar with the internals, so I'm not sure how references are handled here. As for behavior, elements could be merged, with newer elements overriding previous ones. This might also be used to remove bindings.

'Fusion Obby' example place should use framerate-independent particle physics

Currently the Fusion Obby example place has a confetti particle system which is designed to run on a fixed timestep, but which currently just runs every render step. This means the effect may not appear at the correct speed for people not running Roblox at 60fps.

@\boatbomber's Lua Learning already has framerate-independent particle physics, however I'd like to investigate performance-light enhancements to improve the smoothness of the motion when using a fixed time step.

This isn't something I have time to address now, but worth noting to fix later.

Move away from TestEZ for unit testing

While TestEZ has been a good help so far in ensuring the correct functioning of Fusion and giving more confidence about not accidentally introducing regressions, it's starting to get awkward to use it. Other than being an external dependency on Fusion which is something we try to avoid, it also relies on function environment manipulation and documentation is not easy to come by.

In general, it'd be nice to build a solid unified tool for testing Fusion with - we already have our own benchmarking solution which could use some polishing up, so we could explore building up our own framework for running the simple kinds of testing and benchmarking we use.

Add attributes support to New

Currently it's not possible to set attributes on instances being created with the New function. Additionally, it's not possible to listen for changes on attributes.

While attributes aren't particularly useful within Fusion, having support for them would make it easier to integrate Fusion with legacy UI codebases which depend on them.

Oklab colour blending

Currently, Fusion uses a relatively standard CIELAB implementation when blending colours, e.g. for tweening and spring simulation. However, it may not have ideal perceptual uniformity; specifically, it may have hue shift issues:

image

In a future release, Fusion should consider adopting Oklab as the standard colour space for blending between colours. This would likely replace the CIELAB implementation completely.

Given that this is an internal change, this wouldn't have any breaking effects on any APIs, though colours output from animation APIs would be subtly different.

Related:
https://bottosson.github.io/posts/oklab/
https://raphlinus.github.io/color/2021/01/18/oklab-critique.html

Should State() be renamed?

Currently in Fusion we have two bits of conflicting terminology - we refer to all objects that store state as 'state objects', however one of these objects is the State object.

To clarify the difference: Computed, Tween, Spring, Timer etc are state objects, but they are not the same as the State state object.
(this post hurts to write, which is evidence for why this needs to change)

I've been thinking about renaming these to Variable or Value objects, because they store a single user-settable value. This is a (very) breaking change, but should be resolvable with a bit of Find and Replace, and perhaps some conflict resolution for the unfortunate few cases where people are already using Variable or Value as an identifier.

Computed/ComputedPairs/other reactive graph objects should error on yield

Yielding is not allowed during reactive graph updates in order to ensure that the entire reactive tree is internally consistent. However, there's nothing currently stopping users from attempting to yield inside a computed callback.

Yielding like this can break a lot of things that assume no yielding occurs, for example the automatic dependency manager.

This isn't a blocking issue for initial beta release, but needs to be implemented before we move out of beta as it's an important error to catch.

InstanceProperty(instance, propertyName) state-like helper

Currently it is quite annoying in Fusion to base a Computed off of an instance property. It usually ends up being wrapped in a heartbeat updating state. It also easily causes footguns with #44 if it is a table generated based off of the property.
This state-like would use either GetPropertyChangedSignal or Heartbeat (and maybe automatically choosing between those two) to update the state's dependents when the property changes.
Adding this would probably be subject to #40

Shorthand methods for States

Metamethods such as __add, __sub, __mul, etc. could be added to States to allow for shorter code in certain situations such as the following

counter:set(counter:get() + 1) --> counter:set(counter + 1)

In situations where the State is a table the following situations could be shortened

local curList = list:get()
table.insert(curList, "foo")
list:set(curList)
--> list:insert("foo")

local curList = list:get()
table.remove(curList, 1)
list:set(curList)
--> list:remove(1)

table.find(list:get(), "foo")
--> list:find("foo")

Native Instance Support

It would be nice for fusion to have native Instance support, and by that I mean the ability to use existing instances with Fusion. This would cut out the need to set every property inside fusion, and create a object inside studio, and only update properties that need to be updated in real time.

For Example:

local New, State = Fusion.New, Fusion.State
local TextState = State("Some text here :)")

New(existingInstance) {
    Text = TextState
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.