This is a work-in-progress documentation for a themeable GUI prototype for Terasology, based on this forum thread and this forum thread.
In order to provide a more easily customizable GUI, the current user interface system is to be enhanced by allowing the user interface definition through JSON files, separating the user interface logic from its actual appearance.
The current user interface system is roughly devided into the following packages:
-
gui.framework
: Contains basic, abstract GUI functionality that is to be re-used throughout the entire UI. A notable subclass isUIDisplayContainer
which is the base-class for all UI elements. -
gui.widgets
: Contains concrete implementations of UI elements, such asUIButton
orUIText
. -
gui.menus
: Contains structural menu definitions, i.e. what elements a certain menu contains and how the components making up that menu are configured.
The classes within gui.menus
are to be completely replaced by JSON-style definitions.
The visual settings within gui.menus
are to be completely replaced by JSON based definitions. The currently existing classes, such as UIMenuMain
, shall contain only the event handling code, e.g. for click events on buttons.
-
How should the visual states of UI elements be implemented, that is, for example, how should we implement a button switching to hover state and having the hover properties automatically applied to it.
-
If an invalid or unsupported property is used in the style definition, should there be an error or should it be ignored?
User interface definition are stored in JSON files. There are two types of definitions:
- Style definitions, using the file extension
.style.json
. - Layout definitions, using the file extension
.layout.json
.
Both layout and style definitions will have a separate asset type, making them easily loadable by the existing asset system (Source).
Parsing the JSON files should be implemented using the already employed GSON parser, which Immortius knows well. A solution for the "JSON String -> Enum Value" problem is explained here.
Style definitions define the appearence of UI elements, and are re-usable in layout definitions. The style definition is designed to replicate CSS properties, so that it is easily adoptable. An example of a style definition might look like this :
{
"style": {
"size": [120, 100],
"verticalAlign": "center"
}
}
As Adeon stated, the system should stick close to the property names used by CSS, since it'll lower the barrier of entry, but only if the properties actually behave like they do in CSS.
TODO: Add missing, needed properties from here. {: .callout .alert}
There are two types of style properties that can be used on widgets, "global" and "local":
-
Global properties are available for every UI widget (but not necessarily for window styles!), and are implemented in
UIDisplayContainer
or one of its parent classes. -
Local properties are those which are implemented in a specific UI element, such as
UIButton
.
A property for a UI element exists if there is a setter method that adheres to the naming convention set$PropertyName
, e.g. if a UI element class has a public method setFoo()
with a single parameter, it would have a property foo
.
Additionally there are properties for the window, which are made available by the UIWindow
class or its parents.
If any property is used in style definition which does not have an equivalent setter in the UI element, an exception MUST be thrown.
The layout definitions specifies the structure of a menu, e.g. the main menu. It contains an entry for each UI element which is to be displayed.
JSON layout definitions are loaded into the LayoutDefintion
class, which can then instantiates either a UIWindow
or a subclass of a UIDisplayContainer
.
An example for a layout definition:
{
"id": "main-menu",
"controller": "MainMenuController",
"children": [
{
"id": "start-game-button",
"component": "UIButton",
"style-class": "default-button-style",
"style": {
"position": [100, 100]
"text": "Start Game"
}
}
{
"id": "exit-game-button",
"component": "UIButton",
"style-class": "default-button-style",
"style": {
"position": [100, 150],
"text": "Exit Game"
}
},
{
"id": "version-number",
"component": "UIText",
"style": {
"text": "v0.1",
"position": [200, 200]
}
}
]
}
The root object has three mandatory parameters:
-
id: The ID of the menu. This string must uniquely identify the menu so that it may be referenced within the code.
-
controller: The controller implements the event handling logic for a menu and its widgets.
-
children: An array of children UI elements for this menu. These have a numer of required and optional arguments explained in the following sections.
This part (events) is currently postponed until the central parts of the JSON GUI system are in place. It may then be re-evaluated whether or not this is necessary and how much work it'd be to re-structure the existing event handling mechanism. {: .callout .danger}
Every component may support a number of events, i.e. click
or hover
. By default, all events will be delegated to the layout controller's handleEvent
method. If an element is often re-used across multiple layouts, a per-element controller may be defined.
In order to avoid large switch statements in the handleEvent
method, one can specify event handler methods for every UI element using the events
property, as illustrated above in the start-game-button
. Using that technique will automatically invoke the startButtonClick
method with the following signature:
public void startButtonClick(UIDisplayContainer element);
The controller then receives events from the UI elements and executes code accordingly, that is, it adds behavior to the menu. A controller for the exit-game-button
might look like this:
public class ExitGameButtonController implements UIController {
public void handleEvent(UIDisplayContainer element, String eventType) {
// Behavior!
}
}
Where the UIController
interface would look as follows:
interface UIController {
public void handleEvent(UIDisplayContainer element, String eventType);
}
-
id: The ID is the only way to access a UI element from code after creation.
UIWindow
features a methodgetChild(Class type, String id)
for that purpose. -
component: Specifies which
UIDisplayContainer
implementation shall be used to render the element.
-
style: Used to set style parameters directly in the layout definition, overriding those from any
style-class
that may have been specified. Also see Style definition. -
style-class: The style class to be loaded for this element.
Incomplete! {: .callout .danger}
This is the base class for all UI widgets. That is, when specifying a child in a layout definition, a subclass of this class will be instantiated (which one is indicated by the "component" field).
Incomplete! {: .callout .danger}
The controller is where the event (and any other) logic for a menu and its children is implemented. When a window is created from a layout definition, the specified controller is instantiated after all children have been created and all styles have been applied, and its registerEventHandlers
method is invoked. Access to the window (and thus to it's children, retrievable by ID) is available to the controller's "window" field.
- In the JSON definition, the elements are added to the UI in the same order they are listed in the file. Elements added later are rendered above those elements that have been added earlier. This means that if you have, for example, a background image filling the entire screen, this image must be added first, or all elements that were added before will be covered by the background image.