Guide
Components
A component is a .dcue file with up to three parts: a <script lang="cs">, markup, and a <style>. The editor compiles it into a C# class with the file's name.
The file
<script lang="cs">
using DevCore.UiEngine.Input; // usings first: every type the script names needs its own
import Badge from "./Badge.dcue"; // child components: the tag <Badge> and the class Badge
[Prop] public string Title = "Untitled";
[State] int clicks;
[Derived] string label => clicks == 1 ? "1 click" : clicks + " clicks";
void Press(PointerEvent e) => clicks++;
</script>
<article class="card" onpointerdown={e => Press(e)}>
<h3>{Title}</h3>
<Badge Text={label} />
<slot />
</article>
<style>
.card { padding: 16px; border-radius: 12px; background: #1b2338 }
h3 { margin: 0 0 8px }
</style>
The script is a C# class body: fields, methods, nested classes, static members and constants are all allowed. A .dcue file in an app folder, or in a folder with a devcore-ui.json ({ "namespace": "MyGame.UI" }), is a component; anywhere else it is a page, a whole HTML document.
The script
| Declaration | Meaning |
|---|---|
[State] int count; | Reactive state: changing it updates what reads it |
[Derived] string label => ...; | Computed from state, cached, read-only |
[Prop] public string Title = "x"; | Input from the parent |
[Prop, Bindable] public float Value; | A prop the child may change; the parent binds it with bind:Value={x} |
[Effect] void Sync() { ... } | Runs after the page updates, and again when what it read changes |
[Tick] float timeLeft => Round.TimeLeft; | Read every frame; the page updates only when the value changed |
OnMount() / OnDestroy() | Lifecycle |
The full list, with signatures, is in Component script.
Usings and imports
The script names other types as any C# file does: with using lines at its top, beside the imports. The generated class has exactly those lines and the compiler adds none, so a name without its using is the usual C# error, reported at its line of the .dcue file. The attributes of the table and T(...) need none.
An import names its component in the whole file: as a tag in the markup (<Slider />) and as the component's class in the script (Slider slider; for bind:this={slider}), with no using for it.
using | For |
|---|---|
System | Math, Action, Func, DateTime |
System.Collections.Generic | List<T>, Dictionary<K, V> |
UnityEngine | Time, Mathf, Debug |
DevCore.UiEngine.Dom | Element, Node, Text, Document |
DevCore.UiEngine.Dom.Events | Event, EventType |
DevCore.UiEngine.Input | PointerEvent, KeyboardEvent, WheelEvent, FocusEvent, InputEvent, pointer capture |
DevCore.UiEngine.Reactivity | StateList<T>, StateDictionary<K, V>, Signal<T> |
DevCore.UiEngine.Localization | Locale |
DevCore.UiEngine.View | GetBoundingClientRect, ElementFromPoint |
DevCore.UiEngine.Css.Properties, .Css.Values | CssPropertyId, CssUnit (typed styles from code) |
DevCore.UiEngine.Components | ContextKey<T> |
The markup
Markup is HTML with expressions in braces. The most used forms:
<p>{gold:N0} gold, {time:F1} s</p> <!-- text, C# format rules -->
<div class="bar" class:low={health < 25}> <!-- toggle a class -->
<div class="fill" style:width="{health}%"></div> <!-- typed inline style -->
</div>
<button disabled={!canRefresh} onclick={Refresh}>{T("shop.refresh")}</button>
<input bind:value={name}> <!-- two-way binding -->
{#if items.Count == 0}
<p>{T("shop.empty")}</p>
{:else}
{#each items as item, i (item.Id)}
<ItemCard Item={item} OnBuy={() => Buy(item)} />
{/each}
{/if}
Every form is listed in Template syntax.
Lists
A keyed {#each} moves rows instead of rebuilding them. Inside it, handlers always see the row's current item and index, so onpointerdown={e => Grab(e, i)} stays correct after rows move.
A list of thousands of rows (scores, inventory, chat) belongs in {#virtual}: elements exist only for the rows that show.
<div class="list"> <!-- needs a height and overflow: auto -->
{#virtual players as player, i (30)}
<div class="row">{i + 1}. {player.Name} <b>{player.Score}</b></div>
{/virtual}
</div>
The number in parentheses is the height of a row in CSS pixels; all rows share it.
Scoped styles
A component's <style> applies to its own elements only. :global(...) reaches outside, and CSS variables pass through, so a parent can theme a child: <Card --card-bg="#222" />. What every component should share (fonts, colors, resets) goes into the app's global style sheet, App.css.
Props, bindings and slots
<script lang="cs">
using System; // Math
using DevCore.UiEngine.Dom; // Element
using DevCore.UiEngine.Input; // PointerEvent, SetPointerCapture
[Prop] public string Label = "";
[Prop, Bindable] public float Value;
float startX, startValue;
void Press(PointerEvent e)
{
startX = e.ClientX;
startValue = Value;
((Element)e.CurrentTarget).SetPointerCapture(e.PointerId);
}
void Drag(PointerEvent e)
{
if (!((Element)e.CurrentTarget).HasPointerCapture(e.PointerId)) return;
Value = Math.Clamp(startValue + (e.ClientX - startX) / 200, 0, 1);
}
</script>
<label>{Label}</label>
<div class="track" onpointerdown={e => Press(e)} onpointermove={e => Drag(e)}>
<div class="fill" style:width="{Value * 100}%"></div>
</div>
<Slider Label="Volume" bind:Value={volume} />
The parent's content goes where the child writes <slot />; <slot name="footer" /> takes the content given for that name.
Game state in plain classes
Plain C# classes can hold reactive state too. A partial class with [State] fields gets reactive properties from the engine's source generator:
public partial class ShopItem
{
[State] int stock; // becomes the reactive property Stock
[State] int price;
}
For state that several screens show (gold, health, inventory) this is often the simplest design: the game owns the object and passes it to the UI as a prop, the game changes it, and every screen that shows it updates. The game code does not depend on how the UI is built.
Reaching a component from game code
Put query:api on an element or on a component's tag, and find it with Q<T> (the first match) or Query<T> (all of them):
<section class="shop" query:api="shop">
<span query:api="gold-label">{gold} gold</span>
{#each items as item (item.Id)}
<ItemCard query:api="item-{item.Id}" Item={item} />
{/each}
</section>
var shop = app.Q<Shop>("shop"); // the component: its [State], props and methods
shop.gold += 100; // [State] public int gold: the page updates this frame
var label = app.Q("gold-label"); // an element: style, classes, attributes, events
var sword = shop.Q<ItemCard>("item-1"); // inside the shop only
var cards = app.Query<ItemCard>(); // every ItemCard
Fields set from outside must be public. Names are indexed: a search by name visits only what has that name and allocates nothing. Details are in DOM, styles and queries.
Pages with component tags
A page's HTML, loaded at runtime, can use components by name: <Shop></Shop>, <Card Title="Sword"></Card>. Attribute strings are converted to the props' types. Standard HTML element names always win over component names.
Not supported
Slot props (let:), dynamic tags and dynamic components.