elttob / fusion Goto Github PK
View Code? Open in Web Editor NEWA modern reactive UI library, built specifically for Roblox and Luau.
Home Page: https://elttob.uk/Fusion/
License: MIT License
A modern reactive UI library, built specifically for Roblox and Luau.
Home Page: https://elttob.uk/Fusion/
License: MIT License
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:
In addition, the extension story for Fusion is rather unclear:
Given these points, it might be worth moving Fusion to a more modular design. Here's a rough idea of how that could work:
Is this worth pursuing?
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.
These are super flexible (probably more so than springs) which is why they should be added.
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
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.
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.
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
}
There are currently a few formatting decisions in Fusion, that are not widely used, for a few reasons:
if condition then return end
and function() end
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.
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:
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.
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.
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:
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
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:
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 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)
}
}
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.
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"}
})
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:
Example usage:
local conn = RunService.RenderStepped:Connect(...)
local ins = New "Folder" {
[Cleanup] = {conn}
}
ins:Destroy() -- disconnects 'conn'
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.
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.
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:
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.
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.
Perhaps this could be implemented with an optional delay argument on the Spring:get() method?
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.
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
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.
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.
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.
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?
I found a potential secret leaked within the repository, please create a SECURITY.md
file so I can report this.
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
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.
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")
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 :)
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.
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.
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"})
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})
(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"})
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.
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", {...})
})
Reported and repro'd by @/boatbomber
ComputedPairs currently has an undiagnosed memory leak - possibly may be related to automatic dependency management?
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.
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.
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:
New
- it's impossible to know if other connections exist, which could lead to unexpected behaviourI'm currently leaning towards 'no', but it'd be interesting to hear other people's thoughts on this anyway.
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:
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:
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)
}
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
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
}
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.
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).
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.
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.
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!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.