Skip to main content

Template Syntax

EvolveUI has a very sophisticated template engine that takes in .ui template syntax and converts it to IL code that is executed. There are two ways this can happen, either at application start up time or as a build step. When running in dynamic mode, all the templates are parsed and MSIL is generated for them (assuming there weren't any compilation errors)

When pre-compiling we generate real C# code. This means we can fully support any AOT platforms and even run in Unity's il2cpp mode.

Performance between these two modes should be relatively similar, though pre-compiling runs a bit faster as it's better at devirtualizing functions, performs more inlining and will allocate less dynamic memory at runtime. Typically in development you would use dynamic mode because it allows you to hot-reload the entire UI when you change a template or style file. The exception to this rule is debugging. If you run into an issue with your templates that you'd like to debug, it may be easier to generate the code and then set a breakpoint in the generated code.

Ok, on to the fun stuff!

Exploring Template Syntax

A template is defined in a .ui file and describes the structure of a UI and provides a super easy method to handle data binding between your UI hierarchy and your logic.

Data Binding

Any field, property, method, event, or value you use in data binding expressions will need to be public. The reason for this is that when the code is pre-compiled to C#, it will not compile if you use non accessible values.

In general you want to keep the data flow of your UI in a top down direction. There are use cases for two way binding on some elements like TextInput or Dropdown but in general you should strive to pass data down to children and not up to parents.

Template Syntax

Templates can be defined syntactically with a functional notation or a long form structured notation. We'll focus on the structure notation for now. Throughout these examples we'll be building a totally fictional inventory system UI.

template Inventory {

}

template Inventory {

// inside of a template there is a 'render' block that is used to render other UI elements
// We'll start simple and just render a static piece of text
render {
Text("My Inventory");
Text("I have lots of items");
}
}

template Inventory {

// we define a piece of state local to this instance of our UI element to track how many items are in our inventory
// state is defined with a 'state' keyword
state int itemCount;

render {
Text("My Inventory");
Text($"I have {itemCount} items");
}
}


Companion types

A companion is a C# type that supports integration with a ui template. It can be a struct or a class.


// in a C# file somewhere
public struct InventoryCompanion {

// any public fields, methods, properties, or events are visible to ui templates
public float someField;

// defining a field of type ElementReference and adding an [InjectElementReference] attribute will automatically
// link this field to the ui element of the template this companion is bound to
[InjectElementReference]
public ElementReference elementRef;

// [InjectUIRuntime] will automatically set this fields value to the UI runtime to which the bound template belongs
[InjectUIRuntime]
public UIRuntime runtime;

public int ComputeItemCount() { ... }

// any event handler method can be marked with an attribute to implicitly register itself as a handler for the
// corresponding event type. The method must conform to the given event handler signature, in this case accepting either
// a single MouseEvent parameter or no parameters. The method names do not matter, only the signature and the attributes.
[OnMouseDown]
public void OnMouseDown(MouseEvent evt) {
...
}

[OnMouseUp] // The MouseEvent parameter can be omitted if it is not used
public void HandleMouseUp() {
...
}

// Similar to input event handlers, life cycle events can also be declared on a companion type with attributes
// life cycle handlers do not accept any parameters, and can declare any return type they like (which is ignored by the template system anyway)
[OnUpdate]
public void Update() {
...
}

}


// in a .ui file

// a companion is defined with the ':' operator and the name of the C# type which will serve as the companion
template Inventory : InventoryCompanion {

render {
// anywhere inside of a template declaration, '$companion' will provide the companion instance.
// any public field, property, method, or event can then by accessed from that instance.
Text($"Inventory has {$companion.ComputeItemCount()} items");
}

}

Companion Input Events

  • OnMouseDown
  • OnMouseUp
  • OnMouseClick
  • OnMouseHelDown
  • OnMouseEnter
  • OnMouseExit
  • OnMouseContext
  • OnMouseMove
  • OnMouseHover
  • OnKeyDown
  • OnKeyHeldDown
  • OnKeyUp
  • OnTextInput
  • OnDragCreate
  • OnDragMove
  • OnDragCancel
  • OnDragDrop
  • OnDragFinish

Companion Lifecycle Attributes

  • OnCreate
  • OnEnable
  • OnUpdate
  • OnFinish
  • OnDisable
  • OnDestroy

Parameters

Templates can accept parameters which is the preferred way to get data into them. Parameters can be either declared directly in a template or using a from declaration. They can optionally define a default value. If a default value is not provided, then the user must pass in a value for that parameter or the compiler will emit an error.

There are two types of parameter declarations: required and optional. Required parameters cannot define a default value, but optional parameters can.

Parameters can either define a new field on a template or use a feature called from to alias another expression.

Required parameters must be defined before optional ones. The order in which you define your parameters is also the order in which a caller must provide them if not explicitly referring to them by name.

template ParameterExample {

required Vector3 vec; // define a required parameter
optional float value; // define an optional parameter
optional string name = "EvolveUI"; // define an optional parameter with a default value

optional float valueX from stateVector.x; // map this parameter to stateVector's x field
optional float valueY from stateVector.y = 3.14159f; // map this parameter to stateVector's y field, with a default value
state Vector3 stateVector;

// private parameters are not visible outside of the template definition. A caller can still pass them
// into the template but they cannot be used with the `sync` or `onChange` declarations and cannot be extruded
optional:private string secret;

}

template ParameterUsage {

render {
ParameterExample(vec = new Vector3(), valueX = 10, valueY = 11);
}

}

Top level state

Top level state is accessible everywhere inside of a template. State is public by default which means that when a template is used, public state can be extruded. When using the :private visiblity modifier state fields cannot be extruded.

template StatefulExample : CompanionType {

state float value; // define template-local state field

state float valueWithDefault = 100f;

state:private string privateState = "Not visible outside of this template";

// top level state can also be mapped to a companion field
state string itemName from $companion; // if no field/property name is provided, it assumes you want the 'itemName' field/property from the companion
state string itemRarity from $companion.rarity; // if a field/property name is provided, that will be used

// you can both map a companion field and provide a default value like this:
state int itemValue from $companion = 100;

}

Computed Properties

A computed property is a read only method which can be extruded like a value. Computed properties are public by default but can be annotated with :private to make them only visible inside the defining template. They cannot be used with a from mapping but do have access to any top level field declaration.

template ComputedPropertyExample {

state int x;
state int y;

computed int sum => x + y;

// only visible inside this template's definition
computed:private int product => x + y;

}

template UsingAComputedProperty render {

// a computed property can be extruded with the [] operator
ComputedPropertyExample() [sum]

Text($"x + y is {sum}");

}

Methods

Methods can be defined with any signature you like. By default they are public and can be extruded. Methods are available within other methods as well, and can be sourced with a from mapping or defined in the template definition itself.


template MethodExample : SomeCompanionType {

// when defining a method from a companion type you must define the signature explicitly
// and it must exactly match the method as it was defined in the companion type
method float Sqrt(float input) from $companion;

method float PrintSqrt(float input) {
// using the Sqrt method
Debug.Log(Sqrt(input));
}

method void PrintValue(int value) {
Debug.Log(value);
}

}

template MethodUsage render {

// methods get extruded like normal with the [] operator
MethodExample() [PrintValue, PrintSqrt];

run PrintValue(100);
run PrintValue(200);
run PrintSqrt(300f);

}

Extrusion

Where parameters are the way to get data into a template, extrusions are the way to get data back out. You can think of extrusion the same way you think about deconstruction in some languages.

Any public state, required, optional, computed, or method can be extruded, along with a reference to a template's UI element.

Extrusion is performed using the [] operator, which accepts a list of expressions to extrude.

template ExtrusionExample render {

ThingWithInternalState() [value1, value2] {
run Debug.Log(value2);
}
// extrusion scope is not constrained to the child scope created with `{}`
run Debug.Log(value1);

// sometimes there may be naming conflicts with extruded values. When this is the case, you can
// an assign an alias to the extruded value with the 'as' operator

state float xyz;
SomeElement() [xyz as abc]; // alias SomeElement.xyz as abc because xyz was already defined in this scope

// an element reference is extruded with the '&' operator and any unique identifier that is valid in the scope
SomeElement() [&elementRef]
run Debug.Log(elementRef.GetAttribute("selected"));

}

Decorators

Decorators

Decorators are ways to inject behavior into elements that are already defined. An example use case might be that we want to track some analytics to see how often certain UIs are being used, or we want certain actions to trigger a route state change, or we need a way to add tooltips to elements without refactoring the elements to be tooltip aware.

template DecoratorExample render {

// lets turn this button into something that is able to switch between menu screens.
// We could add a click handler here that does this logic, but it would be better if we
// could hook into a menu transition system that we previously created.
Button("Take me there!");

// using a decorator (which can be user defined) we add functionality that intercepts the button click
// and invokes our route transition instead. We didn't even alter the button to do this
@RouterLink("/game/main_menu")
Button("Take me there!");

// Decorators can also accept arbitrary bindings. In this case I extended the Button to also track how many times it was clicked,
// And setup an analytics category with an identifier. Button itself didn't change at all
@TrackClicks(category = "Transitions", identifier = "Go To Main Menu")
Button("Take me there!");

// We can combine as many decorators as we like. Here is the button with both analytics and routing attached
@RouterLink("/game/main_menu")
@TrackClicks(category = "Transitions", identifier = "Go To Main Menu")
Button("Take me there!");

// decorators can also work with extrusion the same way templates do
@TrackClicks(category = "Transitions", identifier = "Go To Main Menu") [totalClicks]
Button("Clicked: " + totalClicks + " times");

}

// you can also define decorators on template and typography declarations, which has the same effect as placing them at the usage site
// like we did in the above examples. Decorators used this way can accept values computed from the template's state and parameters
// and can extrude values that are usable within the template definition.
@RouterLink("Game/Inventory")
@SomeDecorator(value = someValue) [extrudedThing]
template TopLevelDecoratorExample {

optional int someValue;
computed int computeMe => someValue * extrudedThing;

// ...contents...
}

Decorators are defined in a similar manner to templates. They can declare state, params, computed properties, methods, styles, attributes and a companion type. They can also define input and life cycle handlers. Unlike a template, decorators cannot declare a render block, and cannot themselves be decorated.

decorator MyDecorator {

required string name;

style:BackgroundColor = Color.red;

style = [@one, @two];

method string GetName() {
return $"Hello, my name is {name}";
}

}

decorator DecoratorWithCompanion : CompanionType {

state int frameCount = 0;

state string value from $companion;

attr:someAttribute = value;

before:update => frameCount++;

mouse:down => $companion.HandleMouse($evt);

}

Functional Syntax

Sometimes its annoying to type a full template definition for a template that doesn't declare much.

You can use the template shorthand for these cases which is functionally identical to the long form declaration, but instead of using a block to declare all of the members, you declare a list of statements like a function signature and use the render keyword followed by a block to define the contents.

This functional syntax works for template, typography, and function but not decorator.


template Greeter(param string name) render {
Text($"Hello {name}");
}

Modules

The idea behind modules is that they are portable between projects. They packages templates, styles, and assets together and serve as a basis for resolving imports within .ui files. Modules can have dependencies on other modules.

There are no visibility rules for elements, all template / element declarations are considered to be public.

If a module (A) includes a dependency on another module (B) and (B) declares a dependency on (C), and (A) wants to reference something in (C), (A) must declare a dependency on (C), it does NOT automatically inherit (B)'s dependency on (C)

Modules are file system scoped and cannot be nested. A template & style file belong to the nearest module at the same level or above them in the file tree. If no module asset exists in the project, it is an error.

When resolving names for templates & styles, the module in which the template or style is being used is first searched. If there is a template or style that is found, then it is used. If we didn't find the target in the local module, then all of the auto imports and explicit imports are searched. If there is only one match then it is taken. If multiple matches occur then an error is shown and you need to explicitly disambiguate using a ModuleAlias::YourThing fully qualified identifier.

// some file.ui

import SomeModuleName;
import OtherModule as Lib; // import 'OtherModule' and alias it as 'Lib'

// A Module (which is an asset in Unity) can define a list of auto imports
// and these function as though the user had written `import SomeModule`
// in every file belonging to that module

template Something render {

// the scope resolution operator is used to reference elements residing in imported modules
SomeModuleName::Button();

Lib::Button(); // using the alias
OtherModule::Button(); // using the full name, same result as the line above

Button(); // if no scope resolution operator is present, resolution is as follows
// 1. If the 'current' module in which this file is defined defines
// a template called 'Button', then we us it.
// 2. If the current module does not define 'Button', we look all the
// imported modules declared in this file and search for 'Button'.
// If one is found, we use it. If multiple imported modules define
// 'Button', we throw an Ambiguous import error and the user is required
// to use the :: operator to specify which version of 'Button' to use

}

Companion Types

A companion is any type that is declared in C# that is bound to your template, typography, function or decorator declaration. The purpose of a companion is to provide a generic way to integrate C# code with a template.

A type is valid as a companion as long as it is public and defines a parameterless constructor or is a struct.

// C# type somewhere in your project

public struct SomeCompanion {

public int hitPoints;

public string currentLevelName;

public void ReloadWeapon() {
// assume this does work in your game
}

}

// ... some template file ...

// : typeName syntax declares a companion type binding for this template
// there can only be 1 companion per template. When declaring a companion type,
// a new built-in variable becomes available in the template: $companion.
// this refers to the companion instance for this template. From this reference
// you can use any public field, method, property, or event that the type exposes.
template ThingWithCompanion : SomeCompanion {

render {

Text($"Current Level is {$companion.currentLevelName}")

HealthBar($companion.hitPoints);

// you can call methods exposed on the companion instance
Button("Reload", mouse:click => $companion.ReloadWeapon());

}

}

Mapping values from companions

You can map values from a companion in the same way you could map them from state.

// c# file 
public class Inventory {

public int selectedItemIndex;

public string[] GetItemNames() { ... }

public string GetEquippedItemName(int itemIndex) { ... }

}

// template file
template InventoryUI : Inventory {

// we alias the selectedItemIndex parameter from the selectedItemIndex field on the companion
required int selectedItemIndex from $companion.selectedItemIndex;

// when mapping from a companion, if the companion's field name matches the template's declared name
// then you can omit the field name. This line is the same as the one above but shorter.
required int selectedItemIndex from $companion;

method string[] GetItemNames() from $companion; // when mapping a method, the signatures must match exactly.

// you can pick a different name for the template value if you specify the source explicitly;
method string GetItemName(int itemIndex) from $companion.GetEquippedItemName;
}

Mapping event and life cycle handlers from companions

An event or life cycle can also be mapped from the companion. As long as the signatures match between the event you are mapping and the method on the companion instance, the mapping is valid.

There is also a shorthand for mapping both input event names and life cycle handlers. For lifecycle events the return type is ignored and can be anything as long as your mapping method defines no parameters.

The name mapping table for lifecycle events is below:

Event NameMethod Mapping Name
before:createOnBeforeCreate
after:createOnAfterCreate
before:enableOnBeforeEnable
after:enableOnAfterEnable
before:updateOnBeforeUpdate
after:updateOnAfterUpdate
before:inputOnBeforeInput
after:inputOnAfterInput
before:finishOnBeforeFinish
after:finishOnAfterFinish
before:disableOnBeforeDisable
after:disableOnAfterDisable
before:destroyOnBeforeDestroy
after:destroyOnAfterDestroy

There is a similar mapping table for input event names, however with the added caveat that signature of your method must also match

Mouse Event Types

Event NameMethod Mapping Name
mouse:enterOnMouseEnter(MouseInputEvent)
mouse:exitOnMouseExit(MouseInputEvent)
mouse:upOnMouseUp(MouseInputEvent)
mouse:downOnMouseDown(MouseInputEvent)
mouse:heldDownOnMouseHeldDown(MouseInputEvent)
mouse:moveOnMouseMove(MouseInputEvent)
mouse:hoverOnMouseHover(MouseInputEvent)
mouse:contextOnMouseContext(MouseInputEvent)
mouse:scrollOnMouseScroll(MouseInputEvent)
mouse:clickOnMouseClick(MouseInputEvent)
mouse:updateOnMouseUpdate(MouseInputEvent)

Keyboard Event Types

Event NameMapping Method Name
key:downOnKeyDown(KeyboardInputEvent)
key:heldDownOnKeyHeldDown(KeyboardInputEvent)
key:upOnKeyUp(KeyboardInputEvent)

Focus Event Types

Event NameMapping Method Name
focus:gainOnFocusGained(FocusChangeEvent)
focus:lostOnFocusLost(FocusChangeEvent)

Drag Event Types

Drag Event Types

Drag create is a special snowflake because it accepts a MouseInputEvent and expects to return an instance of DragEvent. All other drag handlers conform to the same signature requirements. If you omit a drag event type (listed as T in the table below), then these events will fire any drag event type.

Event NameMapping Method NameDescription
drag:createDragEvent OnDragCreate(MouseInputEvent))Fires when a drag could begin. Returning null will not start a drag. Returning any other subclass of DragEvent will being a drag.
drag:move<T>OnDragMove(DragEvent)Fires when a drag of type T moves across this element
drag:hover<T>OnDragHover(DragEvent)Fires when a drag of type T hovers over this element
drag:update<T>OnDragUpdate(DragEvent)Fires when a drag of type T moves or hovers over this element
drag:enter<T>OnDragEnter(DragEvent)Fires when a drag of type T enters this element
drag:exit<T>OnDragExit(DragEvent)Fires when a drag of type T exits this element
drag:drop<T>OnDragDrop(DragEvent)Fires when a drop event of type T occurs on this element
drag:cancel<T>OnDragCancel(DragEvent)Fires when a drag event of type T is canceled on this element

Lifecycle of elements

Evolve's template system works on a concept called Structural Identity. This means that the compiler figures out which 'scope' and element belongs to and ties that element's lifecycle to the scope. Scopes are created whenever there control flow and all elements declared in that scope are bound to its lifecycle.

This means we only create elements when we enter a scope for the first time and we disable or destroy elements when the scope is no longer in use. Every frame as your template execute the system figures out if the control flow being run was also run last frame or not and decides if it needs to create the elements within this scope or if it should use the same instances from last frame. One of the great things about system is that you can declare state variables anywhere inside of a scope and they will retain their values across frames.

Here is how a frame is built for each template in your game.

  • Was this the first time this scope was entered?
    • if so then invoke any created hooks that are defined on the elements
  • Was this created this frame or previously disabled?
    • if so then invoke any enabled hooks
  • Set all of the per-frame 'bindings' for the currently executing element. This includes:
    • parameters
    • non constant styles
    • non constant attributes
  • invoke any before early input hooks
  • process input
  • invoke any after early input hooks
  • invoke any update hooks
  • invoke any after update input hooks
  • recursively visit all of the children of this template
  • invoke any late input hooks
  • invoke any finish hooks
template Example(required bool showGroup1) render {
if(showGroup1) {
// a new scope is created for the true case
// the two text elements below are bound to this scope
// if this branch does not execute in a given frame,
// all elements that are descendents of this scope will be
// disabled (or destroyed if the scope is marked with :destructive)
Text("Element 1");
Text("Element 2");
}
else {
// a new scope is created for the else case
// Element 3 is a member of this scope.
Text("Element 3");
}
}

Destructive Scopes

A scope can be marked as destructive using a :destructive tag after its declaration. Some examples:

  • if:destructive()
  • else:destructive
  • else if:destructive
  • switch:destructive() { .. }
  • case:destructive "condition" { .. }
  • default:destructive { .. }

Some scopes are implicitly destructive, such as those generated by a repeat. In the case of repeat, if the input size shrinks from one frame to another then the extra items that were inside the repeat last frame will be destroyed. If a repeat is disabled due to its parent scope being disabled, its child scopes will be disabled as normal and not be destroyed.

Destroying a View will destroy all of its descendent scopes and their associated elements.

template Example(required bool showGroup1) render {
// because this scope is marked as destructive, if there is a frame in which
// this branch is not entered, its descendents will all be destroyed instead of
// being disabled as they normally would be. When the branch is later re-entered
// the elements we be re-created again.
if:destructive(showGroup1) {
Text("Element 1");
Text("Element 2");
}
}

Asynchronous Lifecycle

There are two classes of events which are not executed as your code runs each frame: disable and destroy.

For performance reasons we only run these handlers at the start of the next frame. It is illegal / undefined behavior if you have a disable/destroy handler that uses a closure to close over any fields / state in a template. Generally, you should not have to worry about this because the system makes it very hard to do, but in case you are tempted to trick the system into doing something crazy: you've been warned!

Element References

Evolve is a high performance UI library and as such makes certain decisions around what the concept of a UI element really is. Unlike most other UI systems, the core unit of Evolve is not a tree node that is subclassed from some type, but a simple 4 byte type called ElementId.

Because we don't use real objects to represent our UI elements, we need a way work with ElementIds in the context of our game in order to set styles, attributes, perform layout and do all of the typical UI things. The solution to this in Evolve is a type called ElementReference.

The purpose of an ElementReference is to be the one stop shop for interfacing with the system data for a single element. An ElementReference can be used to query layout results, set style properties, manipulate a list of applied styles, check the 'live-ness' of an element, and is often passed around in templates as arguments to things that need to position themselves relatively or query an element for certain properties.

There are a few ways to get an ElementReference depending on the context you are working in.

Template & Typography & Decorator declarations

When inside of a top level declarations (except for function) you can always use $element to get the current element reference. Inside of a render block, $element becomes contextual based on where it is used and is only valid inside a binding expression. It is not valid inside a create, run, enable, var, state, repeat, if or any other expression that is not related to property or lifecycle binding.

In a given element usage like Text("xyz"), the $element reference becomes available inside of bindings.

When inside of a render block, you can refer to $root to get an ElementReference for the current template root, in the example below it would point at Thing.

You can also use $parent to refer to an element's parent. This is available everywhere inside a render block and always points to the currently executing element's parent.

template Thing {

before:update => {
$element.SetAttribute("ticked", true); // in a top level declaration $element points to Thing
}

render {

Text($"the element is {$element}"); // refers to the Text element
Text($"the element is {$root}"); // refers to the Thing, which is the template root

run $element.SetAttribute("illegal", true); // $element points nowhere, this is not allowed
run $root.SetAttribute("illegal", false); // $root points to Thing, totally fine

run $parent.SetAttribute("fine, "yep"); // $parent points at Thing's parent. this MAY be invalid if Thing is a view root and has no parent.

Element1(value = $parent) { // $parent == Thing
Element2(value = $parent) { // $parent == Element1
}

}
}

Inside of a decorator, $element points to the element being decorated.

decorator SomeDecorator {

before:update => $element.stlye.BackgroundColor = `red`;

}

Element references with companion types

When defining a companion type in C#, you can define a public, non readonly field of type ElementReference and mark it with the attribute [InjectElementReference] and the system will set the value of this field to the paired ElementReference.

// when used as a companion, the myElementReference will be set by the system. This still works even if this companion type
// is used as a companion for a decorator. Note that it will NOT be set when used as the companion of a `function`, because functions
// are not mapped 1-1 with elements. No error will be thrown in this case, the field will simply not be initialized.
public class SomeCompanion {

[InjectElementReference]
public ElementReference myElementReference;

}