The Chilli Source UI module aims to make developing multi-device UI easy. CS UI borrows some of the concepts from core CS (such as entities and components) and combines them with UI functionality to create a flexible and extensible UI system.
This tutorial will cover the basics of creating some UI and getting it to show on screen. We will show you how to create a simple main menu in a minute but first, the basic concepts:
Widgets
A widget is similar to an entity in core. A widget contains core transform and sizing information and its behaviour is determined by the components attached. Like entity, widgets can be built as parent child hierarchies.
Coordinate system (making you widgets work on different devices)
Widget has a bunch of typical transform changing methods (similar to entity->GetTransform()) but only for 2D transforms. Widgets are always rendered as overlays on top of the 3D scene and in screen space; therefore, the units of the UI system are pixels.
One of the major strengths of CS UI is the ability to mix and match absolute and relative coordinates. Absolute coordinates are in pixels. This will set the widget to 100×100 pixels regardless of screen size:
widget->SetAbsoluteSize(100, 100);
This will offset the widget by 10 pixels horizontally from its parent’s position:
widget->SetAbsolutePosition(10, 0);
Relative coordinates are fractional and are relative to the widgets parent. This will set the widget to be half the width and height of its parent:
widget->SetRelativeSize(0.5, 0.5);
and this will offset the widget from its parent’s position by the entire width of the parent:
widget->SetRelativePosition(1.0, 0.0);
As well as setting the position of a widget as above you can also change its anchors:
widget->SetOriginAnchor(AlignmentAnchor::k_topLeft);
The above will change the pivot of the widget from its centre to the top left. It will now be rendered with its position at top left.
widget->SetParentalAnchor(AlignmentAnchor::k_topLeft);
The above will anchor the widget to the top left of its parent. Whenever the top left of the parent moves the widget will move with it. Any positional offset on the widget will now be from the new anchored position.
An important part of UI is ensuring that images aren’t stretched across different displays. This is known as maintaining the aspect ratio is is achieved using one of the available size policies:
widget->SetSizePolicy(sizePolicy);
The available size policies are:
- k_none: Size is not altered.
- k_usePreferredSize: Uses the size based on the drawable. This is usually the image size.
- k_useWidthMaintainingAspect: Uses the set width but maintains the drawable aspect ratio by changing the height.
- k_useHeightMaintainingAspect: Uses the set height but maintains the drawable aspect ratio by changing the width.
- k_fitMaintainingAspect: Keeps the aspect ratio of the drawable but sizes the widget to best fit within given size (i.e. use for a background but may give empty borders).
- k_fillMaintainingAspect: Keeps the aspect ratio of the drawable but sizes the widget to fill the given size (i.e. use for a background but may exceed the screen).
Components
Components are very much like the components in core but have lifecycle events specific to widgets. Components can not be added or removed from a widget on the fly but instead make up the immutable definition of a widget.
Drawables and drawable components
Drawables govern how a widget is rendered, e.g. as a single image, a 9 patch, a 3 patch or as text. A widget can only be rendered if it has a drawable component, and the drawable component contains the current drawable. Some widgets don’t need rendering (i.e. container views).
Layouts and layout components
Layouts govern how the children of a widget are sized and positioned. The main layouts are HList, VList and grid. All layouts consist of cells and each cell effectively acts as the parent for the widget in the cell; i.e. positions and sizes are relative to the cell and not the parent widget. A widget can have a layout component which in turn holds the current layout.
Widget Definitions
Widget definitions, unsurprisingly, define the behaviour of a widget; including what components the widget has, what properties are exposed, etc. Widget definitions are created as JSON files and therefore allow you to create your own widgets. The built in CS widget types are:
- Widget: A simple container view that can be used to hold other widgets.
- Image: A standard image view.
- Highlight Button: Standard button implementation that highlights on selection and returns to normal on release.
- Toggle Button: A button that toggles state on release.
- H/V slider: An interactable slider bar.
- H/V stretch progress bar: A progress bar in which the bar graphic stretches to fill the background.
-
H/V fill progress bar: A progress bar in which the graphic is revealed to fill the background.
Label: A view for displaying text.
All widget instances are created from definitions with the .csuidef extension.
Here is the definition for a highlight button:
{
"Type": "HighlightButton",
"Components": [
{
"Type": "Drawable",
"Name": "Drawable"
},
{
"Type": "Highlight",
"Name": "Highlight"
}
],
"ComponentPropertyLinks": {
"Drawable": "none",
"Highlight": "all"
},
"DefaultPropertyValues": {
"Name": "HighlightButton",
"InputConsumeEnabled": "true"
}
}
It might look a bit incomprehensible at first but basically means the following:
- The widget type is named “HighlightButton”.
- It has a drawable component.
- It has a highlight component and all its properties can be changed.
- When it is instantiated it will have the default name “HighlightButton” and it will consume user pointer input.
The next stage is to use a definition to create a widget instance. This can be done in code via the widget factory create methods but in this example we will use template files.
Widget Templates
Template files are used to create instances of widget definitions, to set the properties, and create hierarchies. Basically template files are used to create screens, in this case our main menu. Templates have the .csui extension:
{
"Type": "Widget",
"Name": "MainMenu",
"RelSize": "1 1",
"Children": [
{
"Type": "HighlightButton",
"Name": "PlayButton",
"RelPosition": "0.0 0.166665",
"RelSize": "0.4 0.1",
"NormalDrawable": {
"Type": "ThreePatch",
"Insets": "0.25 0.25",
"TexturePath": "ButtonOff.png",
"Direction": "Horizontal"
},
"HighlightDrawable": {
"Type": "ThreePatch",
"Insets": "0.25 0.25",
"TexturePath": "ButtonOn.png",
"Direction": "Horizontal"
},
"Children": [
{
"Type": "Label",
"Text": "Play",
"TextColour": "0.0 0.0 0.0 1.0"
}
]
},
{
"Type": "HighlightButton",
"Name": "OptionsButton",
"RelPosition": "0.0 -0.166665",
"RelSize": "0.4 0.1",
"NormalDrawable": {
"Type": "ThreePatch",
"Insets": "0.25 0.25",
"TexturePath": "ButtonOff.png",
"Direction": "Horizontal"
},
"HighlightDrawable": {
"Type": "ThreePatch",
"Insets": "0.25 0.25",
"TexturePath": "ButtonOn.png",
"Direction": "Horizontal"
},
"Children": [
{
"Type": "Label",
"Text": "Options",
"TextColour": "0.0 0.0 0.0 1.0"
}
]
}
]
}
This template when created will create a fullscreen “MainMenu” container with 2 buttons, each button has a child label – “Play” and “Options”.
You can create a hierarchy by giving a widget a child widget. In CS the depth of a widget is determined by its index in the child list. This means children are always drawn on top of their parent and the last sibling is the top most view.
The rest of the template is basically setting the properties of each widget (including the relative sizes and positions). The most commonly used HighlightButton properties are “NormalDrawable” and “HighlightDrawable”; these are the drawables for the standard and selected state of the button and in this case are 3-patches with a texture (3-patch buttons allow lossless scaling in one direction, in this case horizontal. We could also have used standard or 9-patch and we could have also used a texture atlas rather than a single texture). The label properties are “Text” and “TextColour”.
Widget templates are resources and therefore cached by resource pool. You can use the template to create as many widget instances as you like:
CSUI::WidgetSPtr CreateMainMenu()
{
auto widgetFactory = CSCore::Application::Get()->GetWidgetFactory();
auto resPool = CSCore::Application::Get()->GetResourcePool();
auto templateWidget = resPool->LoadResource(CSCore::StorageLocation::k_package, "GUI/MainMenu.csui");
CSUI::WidgetSPtr widget = widgetFactory->Create(templateWidget);
return widget;
}
We then need to display the widget by adding it to the canvas (the canvas is the UI equivalent of the scene for entities):
auto mainMenu = CreateMainMenu();
GetUICanvas()->AddWidget(mainMenu);
If you run this code your UI will show on screen but your buttons will be useless; let’s fix this:
m_playButton = mainMenu->GetWidget("PlayButton");
m_playButtonConnection = m_playButton->GetReleasedInsideEvent().OpenConnection([](CSUI::Widget* in_widget, const CSInput::Pointer& in_pointer, CSInput::Pointer::InputType in_inputType)
{
if(in_inputType == CSInput::Pointer::GetDefaultInputType())
{
//Do something
}
});
If you’re not familiar with the event connection syntax then check out the events tutorial. What the above code does is grab the “PlayButton” widget by name and listen for the pointer releasing on it (having initially been pressed inside it). To make sure we don’t press the button on right-click for instance we check only against the default input type – left click or touch.
Resolution dependent assets
One of the powerful features of Chilli Source is being able to load specific assets based on the current screen resolution. This is covered in the resource loading tutorial but it makes sense to cover it again briefly here as it is one of the tools for creating cross device UI.
A quick recap of resolution dependent assets is that you can set up resource buckets each banded to a resolution range. Each bucket is given a name i.e. low, med, high. Assets are placed into a bucket by tagging the filename with the bucket name i.e. ButtonOff.high.png, ButtonOff.low.png, etc. The engine will automatically load the correct asset based on the current device resolution.