-
Notifications
You must be signed in to change notification settings - Fork 23
Shine's GUI System
Shine uses its own GUI system that is somewhat separate from the rest of the game. This was done for two reasons:
- The game's code was often changing, and so I did not want to rely on things that may change at any moment.
- I did not like the way the game's GUI was written. It has many annoyances and is hard to work with.
All objects available are stored under lua/shine/lib/gui/objects
and are loaded at client startup automatically.
Each object file defines a meta-table for the object. Instances of the object are simply tables with their object's meta-table assigned.
For actual rendering, SGUI still uses the game's GUIItems. Most controls have a Background
item that serves as the
root of their internal tree.
Layout is handled by a declarative layout engine that is documented on its own page.
It's recommended and convenient to declare SGUI
in the local variables at the top of a file that's using it:
local SGUI = Shine.GUI
References to SGUI
below assume this has been done.
The recommended way to define SGUI elements is using SGUI:BuildTree()
. This function provides a declarative mechanism
to easily specify trees of elements and their layout.
Trees are declared as a list of definition tables, each of which will create a single control or layout element. A
Parent
can be specified to make the tree's top-level elements be children of a given element.
Definitions have the following structure:
{
-- ID allows referencing the created element in the returned elements table (and in OnBuilt callbacks).
ID = "ExampleID",
-- Type can be one of "Control" or "Layout", and is used to determine whether a control or layout is created.
Type = "Control",
-- Class is the name of the class of element to create, under the given type (e.g. "Button" control or
-- "Horizontal" layout). This can also be a table for anonymous control classes created by SGUI:DefineControl().
Class = "Panel",
-- Props are a table key-value pairs that will be set on the created element.
-- Each key must have a corresponding setter method, otherwise it will be ignored.
Props = {
Alignment = SGUI.LayoutAlignment.CENTRE
},
-- Children is an array of child definitions to be created underneath this definition.
-- These child elements are automatically assigned to the layout of the parent, if it has one.
-- If a control has no layout, and its first child definition is a layout, then the child layout is assigned to the
-- control.
Children = {
{
-- Another definition...
ID = "Child",
Class = "Button",
-- ...
}
},
-- Bindings are an array of binding definitions used to glue together properties between elements in the tree.
Bindings = {
{
-- Bindings can refer to other elements in the tree by their ID.
-- Alternatively, an element instance can also be passed here (e.g. self if creating a tree within a
-- control's initialise method.)
From = {
Element = "SomeNamedElement",
Property = "IsVisible"
},
-- To can either be a single property, or an array of properties to bind to, optionally with Filter and
-- Transformer functions.
To = {
Property = "Example",
Transformer = function( IsVisible ) return IsVisible and "Yes" or "No" end
}
}
},
-- PropertyChangeListeners are an array of listeners to attach to properties on the created element.
-- This is equivalent to calling Element:AddPropertyChangeListener().
PropertyChangeListeners = {
{
Property = "IsVisible",
Listener = function( Panel, IsVisible )
LuaPrint( Panel, "is now", IsVisible and "visible" or "invisible" )
end
}
},
-- "If" allows for conditional elements, without having to conditionally populate the tree table first.
-- This is optional, and may be a constant true/false value as well as a callback.
If = function( Definition ) return true end
}
SGUI:BuildTree()
returns a table of elements by their ID. Elements that do not specify an ID
value are not returned.
IDs can be specified at any depth, but must be unique across the entire tree.
For example, to create a window with a label and button:
local Elements = SGUI:BuildTree( {
{
ID = "Window",
Class = "Column",
Props = {
Size = Vector2( 800, 600 ),
Pos = Vector2( -400, -300 ),
Anchor = "CentreMiddle",
-- Add padding to all sides of the window's content.
Padding = Spacing.Uniform( HighResScaled( 16 ) )
},
Children = {
{
Class = "Label",
Props = {
AutoSize = UnitVector( Percentage.ONE_HUNDRED, Auto.INSTANCE ),
AutoWrap = true,
Text = "This is an example label in the centre.",
-- Align the label in horizontally in the centre of the window.
CrossAxisAlignment = SGUI.LayoutAlignment.CENTRE
}
},
{
Class = "Button",
Props = {
Text = "OK",
-- Align the button horizontally too.
CrossAxisAlignment = SGUI.LayoutAlignment.CENTRE,
-- Add some margin between the button and the label.
Margin = Spacing( 0, HighResScaled( 8 ), 0, 0 )
},
OnBuilt = function( Definition, Button, Elements )
-- Close and destroy the window on click.
function Button:DoClick()
Elements.Window:Destroy()
end
end
}
}
}
} )
-- Every element with an ID is accessible from the returned elements table.
SGUI:EnableMouse( true, Elements.Window )
To create an instance of an SGUI control procedurally, use SGUI:Create()
.
-- First argument is the class name, second is an optional parent control.
SGUI:Create( "Button", Window )
The class should be one of the registered object classes, for instance "Button", or an anonymous class definition. The parent is optional, set this if you want the object to move with the given parent object and be positioned relative to it.
Parents of controls must themselves be controls. Parenting to layouts is handled by adding the element to the layout.
For setting properties, you have two choices. Either run the methods individually one after the other, or you can use Object:SetupFromTable
and pass in a table of properties.
local SGUI = Shine.GUI
local Button = SGUI:Create( "Button" )
Button:SetAnchor( "CentreMiddle" )
Button:SetSize( Vector( 128, 32, 0 ) )
Button:SetPos( Vector( -64, -16, 0 ) )
Button:SetText( "Button1" )
local SecondButton = SGUI:Create( "Button" )
-- This behaves similarly to the "Props" table in SGUI:BuildTree().
SecondButton:SetupFromTable{
Anchor = "CentreMiddle",
Size = Vector( 128, 32, 0 ),
Pos = Vector( -64, -16, 0 ),
Text = "Button2"
}
Both buttons above will be identically sized and placed.
To destroy a control, call:
Control:Destroy()
Note that if this control has children, they are all destroyed along with it. Attempting to use a destroyed control
for any operation other than calling Control:IsValid()
or printing the control will result in an error.
Where custom UI logic is required, it is possible to declare anonymous controls that act similarly to named controls,
but are not registered. To do so, use SGUI:DefineControl()
:
local Controls = SGUI.Controls
-- This defines a control named 'Example' that inherits from 'Panel', but is not registered.
-- The name is used by the skin system (see below).
local Example = SGUI:DefineControl( "Example", "Panel" )
function Example:Initialise()
Controls.Panel.Initialise( self )
-- Add custom initialisation logic here.
end
Anonymous control definitions can be passed in place of class names in both SGUI:BuildTree()
and SGUI:Create()
, e.g.
SGUI:BuildTree( {
{
ID = "Example",
-- Pass the definition table that was returned by SGUI:DefineControl() to create an instance of it.
Class = Example,
Props = {
-- ...
}
}
} )
-- Or pass it to SGUI:Create().
SGUI:Create( Example )
Defining anonymous controls has numerous advantages over managing larger trees manually, allowing for internal re-use and encapsulation of UI logic without polluting the default registered controls. Anonymous controls can have their own properties, which can be used with the binding system, and they also receive SGUI events such as keyboard/mouse input.
For examples of this, see the system notifications UI and the base commands plugin's admin menu tabs.
SGUI separates the styling of an element from the control's implementation using a skin system. By default, elements make use of the current global skin that the user has configured.
To provide a custom skin for element-specific styling, define a table with the following structure:
local Skin = {
-- Top-level keys correspond to control names (either registered, or the name passed to SGUI:DefineControl()).
Button = {
-- Each element should have a "Default" key that determines the default styling.
-- Each key-value corresponds to a property that will be set on the element at creation.
Default = {
ActiveCol = Colour( 1, 1, 1 ),
InactiveCol = Colour( 0.8, 0.8, 0.8 )
}
},
CheckBox = {
Default = {
BackgroundColour = Colour( 0, 0, 0 ),
-- Where elements define styling states, overrides can be provided for each state.
States = {
Disabled = {
BackgroundColour = Colour( 0, 0, 0, 0.8 )
}
}
}
}
}
Then pass it to your root element as follows:
local Skin = { --[[...]] }
function Example:Initialise()
Controls.Panel.Initialise( self )
-- Set the skin at initialisation before adding children, this ensures it will be propagated to all children.
self:SetSkin( Skin )
self.Elements = SGUI:BuildTree( {
Parent = self,
-- Create elements...
{
Class = "Button",
-- ...
}
} )
end
To easily fade an entire tree of elements in one go, define the following in a custom control:
function Example:Initialise()
-- Flag the root element as inheriting its parent's alpha.
self.InheritsParentAlpha = true
-- Then propagate that to all child elements recursively as they are created.
self:SetPropagateAlphaInheritance( true )
end
Then to fade the tree in and out, simply apply an AlphaMultiplier
transition to the element:
ExampleElement:ApplyTransition( {
Type = "AlphaMultiplier",
StartValue = 0,
EndValue = 1,
Duration = 0.15
} )
Combine this with the SGUI.Shaders.Invisible
shader on GUIItems that should not render rather than specifying a colour
with alpha = 0.
To make elements smoothly move to their intended positions, use Control:SetLayoutPosTransition()
and/or Control:SetLayoutSizeTransition()
.
These take a transition definition in the same format as expected by ApplyTransition
, but will be applied
automatically whenever the layout the control is within moves or resizes the control.
Some controls provide this in a managed form. For List
controls, use List:SetRowPosTransition()
which will be
applied to all rows automatically.