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

Card.dcue
<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

DeclarationMeaning
[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.

usingFor
SystemMath, Action, Func, DateTime
System.Collections.GenericList<T>, Dictionary<K, V>
UnityEngineTime, Mathf, Debug
DevCore.UiEngine.DomElement, Node, Text, Document
DevCore.UiEngine.Dom.EventsEvent, EventType
DevCore.UiEngine.InputPointerEvent, KeyboardEvent, WheelEvent, FocusEvent, InputEvent, pointer capture
DevCore.UiEngine.ReactivityStateList<T>, StateDictionary<K, V>, Signal<T>
DevCore.UiEngine.LocalizationLocale
DevCore.UiEngine.ViewGetBoundingClientRect, ElementFromPoint
DevCore.UiEngine.Css.Properties, .Css.ValuesCssPropertyId, CssUnit (typed styles from code)
DevCore.UiEngine.ComponentsContextKey<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

Controls/Slider.dcue
<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>
Using it
<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):

Shop.dcue
<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>
Game code
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.