Skip to main content

User Interfaces

Easel provides a simple and powerful way to create user interfaces for your games. These can be used to create standalone webpages surround your game, or create the heads up display or control systems within your game itself. In Easel, user interfaces (commonly abbreviated as UI) are defined in a declarative manner, and can be dynamically updated using a streamlined syntax.

Creating user interfaces

To create a user interface, first create a Section. There are two kinds of sections - content and transmissions:

  • Content (also Toolbar, Modal and some others) is owned by its entity and will be removed when its owning entity despawns.
  • A Transmission will automatically disappear after a certain duration.

Within the section, create elements, such as P for paragraphs or Button for buttons.

pub fn Example() {
Spawn greetingDialog {
Content {
P { "Hello, world!" }
}
}
}

Displaying text

% statements can be used to display any text into the user interface.

pub fn Example(age) {
Transmission {
%("You are " + age + " years old")
}
}

The % can be omitted when only a string literal is being displayed, or when displaying a string concatenation that begins with a string literal.

pub fn Example(name) {
Transmission {
"Hello, world!"
"My name is " + name
}
}
info

The % statement inserts its text into the slot designated by the ui parameter which it finds implicitly from context. You should not need to worry about any of this. As long as you use % inside of a UI function subblock, it should "just work" as expected.

Reactive replacement

In general, the way to update the user interface is to use a loop (or some other construct like a with block) to execute the same code again. The loop will replace the previously-generated user interface elements on each iteration.

pub fn owner.Score = 0

pub fn owner.DisplayScore() {
TopContent {
with Score {
H1 { %(Score) }
}
}
}

Appending elements

Prefix a loop body with %% to append user interface elements on each iteration, rather than replace them.

pub fn Example() {
Transmission {
// without %%, we would update rather than append to the user interface
for i in Range(0,10) %%{
P { "I am element " + i }
}
}
}

Without %%{ ... }, the above loop would first emit "I am element 0" on the first iteration, then replace it with "I am element 1" on the second iteration. Wrapping the loop body with %%{ ... } causes the loop to append to its user interface elements on each iteration, so that "I am element 0", "I am element 1", "I am element 2", etc are all displayed in the user interface at the same time.

This syntax may be used in the same for all forms of loop, behavior and callback blocks.

for i in Range(0,10) %%{ ... }
repeat 5 %%{ ... }
while x < 5 %%{ ... }
once Tick(1s) %%{ ... }
on Tick(1s) %%{ ... }
with Health %%{ ... }
|name, age, ui| %%{ ... }
FunctionWithSubblock %%{ ... }

Measurements

All user interface dimensions are measured in em units, where 1em is the size of the font used in the user interface. The font size is taken from the root of the webpage, and so will not change when temporarily increasing the font size of a single element.

pub fn Example() {
Transmission {
P(fontSize=2) { "This text is twice as big as normal" }
Button(width=25) { "I am a wide button" }
}
}

Stacking layout

Parent elements lay out their child elements in one of two ways:

  • Paragraphs (for example P or Button) lay out each child horizontally one after the other, wrapping to the next line when the line is full.
  • Stacks (for example VStack, HStack or Panel) lay out their children either horizontally or vertically. Wrapping is not allowed. The expand=true property can be used to make one or more children grow to take up all remaining space in the stack.

Paragraphs should be used for text, whereas Stacks should be used for layout. Most stacking elements (VStack, Panel) will stack vertically, except for HStack and Toolbar which stack horizontally. Place an HStack inside of a VStack or vice versa to change stacking direction.

These simple stacking rules allow creating quite sophisticated layouts.

pub fn RenderLeaderboardRow(playerName, userId, rating, [color, ui]) {
ShinyPanel(backgroundColor=color, vPadding=0.4) { // ShinyPanel is a VStack like most other stacking elements
HStack { // Change to an HStack so we can stack horizontally
P(fontSize=1.2, bold=true) {
SubtleLink(ProfilePage(userId)) {
%(playerName)
}
}

Spacer // Take up all remaining space in between the two elements

P { // This element appears on the right because of the Spacer
%(rating.Rating.FormatWithDecimals(0))
" rating"
}
}
}
}

Multi-slot parents

Certain elements have multiple different slots where different sets of children can be displayed. For example, the Tab element has both a header and a body. To populate one of the secondary slots, use the %ui:slotName { ... } syntax:

Tab($fireball) {
%ui:header { // Populates the header slot of `Tab`
"Fireball"
}

P { "Launch a ferocious ball of fire that engulfs your enemies" }
}

Slots

warning

This section contains a technical explanation of how functions populate the user interface. It is not necessary to understand this to use Easel, so don't feel like you need to read this!

In Easel, the UI hierarchy is composed of elements, which in turn may have more elements. Elements that have children are called parent elements. Parent elements generally contain one slot where their children are displayed, For example, the Panel element displays a framed border in the user interface, and then inside there is a slot where it children are displayed inside the panel.

A parent element function calls its subblock to populate its slot with child elements. The subblock is given a ui parameter which identifies the slot where the child elements will be inserted. In order to reduce clutter, the ui parameter is normally omitted from the subblock parameter list and only passed implicitly.

pub fn Example() {
Transmission { SayHello } // implicit ui parameter
Transmission ui { SayHello(ui=) } // equivalent, but with explicit ui parameter
}
pub fn SayHello([ui]) {
%("Hello, world!")
}

The compiler applies special magic to ui parameters. Each time the ui parameter is read, the compiler decorates the value with a unique index within the slot. This way, if the same line of code is executed again, the child element function will replace the previously emitted element because its slot number is the same. This enables user interfaces to be updated dynamically with ease. This magic applies to any function parameter named ui or that begins with the prefix ui: (for example ui:header or ui:whenOn).

pub fn Example() {
Transmission {
H1 { "The great counter" }
P {
let counter = 0
on Tick(1s) {
counter += 1
"We have waited " + counter + " seconds"
// the above text replaces the previous text every second
// because it is written to the same index within the slot
}
}
}
}

When emitting UI elements within a loop, the compiler automatically looks after clearing the elements from the previous iteration before emitting the new ones. This may seem obvious, but if it did not do this, the example below would display both "I am even" and "I am odd" at the same time after the second iteration as the two strings are emitted to different slot numbers. Cleanup is necessary and performed with precision by the compiler so Easel developers never need to worry about this situation.

pub fn Example() {
Transmission {
for i in Range(0,10) {
if i % 2 == 0 {
"I am even"
} else {
"I am odd"
}
await Tick(1s)
}
}
}

The ui parameter is magic, meaning it is given special treatment by the compiler. The magic only applies when ui is a parameter. A local variable named ui is not magic. Reassigning the value of a ui parameter to another variable will not bestow the magic upon the new variable. To keep the magic working, you should basically never touch ui parameters directly.