Skip to content

Shine's GUI System

Person8880 edited this page Sep 2, 2023 · 9 revisions

Overview

Shine uses its own GUI system that is somewhat separate from the rest of the game. This was done for two reasons:

  1. The game's code was often changing, and so I did not want to rely on things that may change at any moment.
  2. I did not like the way the game's GUI was written. It has many annoyances and is hard to work with.

Basic Principles

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.

Creating SGUI Elements

With SGUI:BuildTree()

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 )

Manually Creating Elements

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.

Setting Properties

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.

Destroying an SGUI Control

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.

Defining Custom Controls

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.

Skins

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

Other Tips and Tricks

Fading with Alpha Inheritance

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.

Applying Position/Size Transitions With Layouts

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.

Clone this wiki locally