Button Component
This example is a relatively complete button component implemented using
Fusion's Roblox API. It handles many common interactions such as hovering and
clicking.
This should be a generally useful template for assembling components of your
own. For further ideas and best practices for building components, see
the Components tutorial.
Overview
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114 | local Fusion = -- initialise Fusion here however you please!
local scoped = Fusion.scoped
local Children, OnEvent = Fusion.Children, Fusion.OnEvent
type UsedAs<T> = Fusion.UsedAs<T>
local COLOUR_BLACK = Color3.new(0, 0, 0)
local COLOUR_WHITE = Color3.new(1, 1, 1)
local COLOUR_TEXT = COLOUR_WHITE
local COLOUR_BG_REST = Color3.fromHex("0085FF")
local COLOUR_BG_HOVER = COLOUR_BG_REST:Lerp(COLOUR_WHITE, 0.25)
local COLOUR_BG_HELD = COLOUR_BG_REST:Lerp(COLOUR_BLACK, 0.25)
local COLOUR_BG_DISABLED = Color3.fromHex("CCCCCC")
local BG_FADE_SPEED = 20 -- spring speed units
local ROUNDED_CORNERS = UDim.new(0, 4)
local PADDING = UDim2.fromOffset(6, 4)
local function Button(
scope: Fusion.Scope,
props: {
Name: UsedAs<string>?,
Layout: {
LayoutOrder: UsedAs<number>?,
Position: UsedAs<UDim2>?,
AnchorPoint: UsedAs<Vector2>?,
ZIndex: UsedAs<number>?,
Size: UsedAs<UDim2>?,
AutomaticSize: UsedAs<Enum.AutomaticSize>?
},
Text: UsedAs<string>?,
Disabled: UsedAs<boolean>?,
OnClick: (() -> ())?
}
): Fusion.Child
local isHovering = scope:Value(false)
local isHeldDown = scope:Value(false)
return scope:New "TextButton" {
Name = props.Name,
LayoutOrder = props.Layout.LayoutOrder,
Position = props.Layout.Position,
AnchorPoint = props.Layout.AnchorPoint,
ZIndex = props.Layout.ZIndex,
Size = props.Layout.Size,
AutomaticSize = props.Layout.AutomaticSize,
Text = props.Text,
TextColor3 = COLOUR_TEXT,
BackgroundColor3 = scope:Spring(
scope:Computed(function(use)
-- The order of conditions matter here; it defines which states
-- visually override other states, with earlier states being
-- more important.
return
if use(props.Disabled) then COLOUR_BG_DISABLED
elseif use(isHeldDown) then COLOUR_BG_HELD
elseif use(isHovering) then COLOUR_BG_HOVER
else return COLOUR_BG_REST
end
end),
BG_FADE_SPEED
),
[OnEvent "Activated"] = function()
if props.OnClick ~= nil and not peek(props.Disabled) then
-- Explicitly called with no arguments to match the typedef.
-- If passed straight to `OnEvent`, the function might receive
-- arguments from the event. If the function secretly *does*
-- take arguments (despite the type) this would cause problems.
props.OnClick()
end
end,
[OnEvent "MouseButton1Down"] = function()
isHeldDown:set(true)
end,
[OnEvent "MouseButton1Up"] = function()
isHeldDown:set(false)
end,
[OnEvent "MouseEnter"] = function()
-- Roblox calls this event even if the button is being covered by
-- other UI. For simplicity, this does not account for that.
isHovering:set(true)
end,
[OnEvent "MouseLeave"] = function()
-- If the button is being held down, but the cursor moves off the
-- button, then we won't receive the mouse up event. To make sure
-- the button doesn't get stuck held down, we'll release it if the
-- cursor leaves the button.
isHeldDown:set(false)
isHovering:set(false)
end,
[Children] = {
New "UICorner" {
CornerRadius = ROUNDED_CORNERS
},
New "UIPadding" {
PaddingTop = PADDING.Y,
PaddingBottom = PADDING.Y,
PaddingLeft = PADDING.X,
PaddingRight = PADDING.X
}
}
}
end
return Button
|
Explanation
The main part of note is the function signature. It's highly recommended that
you statically type the function signature for components, because it not only
improves autocomplete and error checking, but also acts as up-to-date, machine
readable documentation.
local function Button(
scope: Fusion.Scope,
props: {
Name: UsedAs<string>?,
Layout: {
LayoutOrder: UsedAs<number>?,
Position: UsedAs<UDim2>?,
AnchorPoint: UsedAs<Vector2>?,
ZIndex: UsedAs<number>?,
Size: UsedAs<UDim2>?,
AutomaticSize: UsedAs<Enum.AutomaticSize>?
},
Text: UsedAs<string>?,
Disabled: UsedAs<boolean>?,
OnClick: (() -> ())?
}
): Fusion.Child
The scope
parameter specifies that the component depends on Fusion's methods.
If you're not sure how to write type definitions for scopes,
the 'Scopes' section of the Components tutorial
goes into further detail.
The property table is laid out with each property on a new line, so it's easy to
scan the list and see what properties are available. Most are typed with
UsedAs
, which allows the user to
use state objects if they desire. They're also ?
(optional), which can reduce
boilerplate when using the component. Not all properties have to be that way,
but usually it's better to have the flexibility.
Property grouping
You can group properties together in nested tables, like the Layout
table
above, to avoid long mixed lists of properties. In addition to being more
readable, this can sometimes help with passing around lots of properties at
once, because you can pass the whole nested table as one value if you'd like
to.
The return type of the function is Fusion.Child
, which tells the user that the
component is compatible with Fusion's [Children]
API, without exposing what
children it's returning specifically. This helps ensure the user doesn't
accidentally depend on the internal structure of the component.