Props & State

Props

Props are typed inputs set by the parent. They are immutable within the component.

tape
component Display {
    prop value: string = "";
    prop size: i64 = 16;
    prop bold: bool = false;
}

The parent sets props at instantiation:

tape
Display { value: "Hello"; size: 24; }
Display { value: count_text; }  // uses default size and bold

Rules:

  • Props with defaults are optional at instantiation
  • Props without defaults are required (compiler error if missing: missing prop 'value' in Display: it has no default)
  • A default is computed for each instance that leaves the prop out, in a view tree, a for list or #code alike. Defaults run in order, after the parent’s values and before the component’s var initializers, so a var can start from a prop and a default can use an earlier prop (prop h: i64 = w / 2;) or any function or constant of the component’s module
  • A string prop keeps its own copy of the string it is given, and so does a prop of a struct holding strings (or an array of them)
  • Props are read-only inside the component
  • Props can be any type: primitives, structs, fixed arrays, slices, pointers, function pointers, color. A struct or array prop lives inside the component, so it reads like a local: pos.x, items.len, items[i], for (it in items), "{name}". A slice prop is a view of someone else’s elements, like any slice
  • Each value is checked against the prop’s type, in a view tree as in code, with the usual conversions (Display { size: "big"; } is an error: prop 'size' is 'i64', got 'string'); a view value can use the component’s props, vars and methods, module globals and functions, and for variables
  • Props are style-targetable — #style can override default values

Props and #style

Props can be set from #style blocks, overriding defaults without the parent explicitly passing them:

tape
component Button {
    prop bg: color = #444444;
    prop fg: color = #FFFFFF;
    prop label: string = "";
}
tape
#style
Button {
    bg: #336699;
    fg: #FCFDFF;
}

The #style values act as overrides to the prop defaults. The parent can still explicitly set a prop at instantiation, which takes priority over the style. Rules are checked against the props’ types and resolved at compile time; see #style.

Reactive state (var)

Mutable var declarations inside a component are reactive state. Any mutation marks the component dirty for re-render on the next frame:

tape
component Counter {
    var count: i64 = 0;
    var count_text: string = "0";

    fn increment() {
        count = count + 1;
        count_text = "{count}";
    }
}

State initializers run once at component creation and can reference props.

A var’s type can be left out when its initializer is a literal that says it: var label = ""; is a string, var count = 0; an i64, var ratio = 1.5; an f64, var open = false; a bool, var at = Point { x: 0; y: 0; }; a Point. Anything else needs the type written (var n: i64 = compute();). A written type is checked like a let’s: var count: i64 = "0"; is a compile error ('count' is i64, given string), and so is var ratio: f64 = 1; (write 1.0).

A var or prop can be optional, ?T: it holds null or a T inside the component, read like any optional (nick == null, nick!, nick or { "anon" }) and assigned null, a T or another ?T:

tape
component Profile {
    prop nick: ?string = null;
    var last_seen: ?i64;
    fn label() -> string { return nick or { return "anonymous"; }; }
    fn seen(t: i64) { last_seen = t; }
}

A var holding a string (or a struct or array with strings in it) owns its value: assigning it copies what you assign (or takes over a temporary, such as a concatenation) and frees the old value; the component’s strings are freed when it is unmounted (ui.unmount(c), which also frees the component itself), or by @drop(c) on a component made in #code:

tape
component Greeter {
    var name: string = "world";
    fn rename(n: string) { name = n; }        // a copy of n; the old name is freed
    fn shout() { name = name + "!"; }
}

Auto-generated setters

For every var field, the compiler generates a set_<name> method that stores the value and marks dirty:

tape
component Slider {
    var value: f64 = 0.0;
    // compiler generates: fn set_value(val: f64) { value = val; /* mark dirty */ }
}

A string var’s setter takes val: []const u8 and stores a copy, so it binds to events that hand a view of a widget’s buffer (a string argument converts to the view). These are useful for binding to events:

tape
widgets.TextInput { value: query; on_change: set_query; }

That is a controlled input: value sets the text shown, on_change reports each edit and set_query writes it back.

If you define fn set_<name> manually, the compiler skips auto-generation for that field.

Dirty marking

The compiler inserts dirty flag writes after every assignment of a var or a state, inside a component method or from outside it through an instance (an element, a field or an unwrapped value of one included: items[i] = v, c.pos.x = v, c.count! = v; = and += alike), after a call of a method taking *self on one or a part of one (items.push(x), o!.push(x), c.items.push(x), c.pos.move(1); a *const self or by-value method marks nothing, and a method called on a var that holds another component’s instance, other.reset(), marks that instance, not the var), and a component whose parent gives it a new prop value is dirty too. Multiple mutations in one handler are idempotent — the component is repainted at most once per frame, and a component nothing changed is not repainted at all (ui: what gets repainted):

  1. Event handler executes (may mutate state multiple times)
  2. Handler returns
  3. Frame tick: runtime walks dirty components, repaints dirty regions
  4. Clear dirty flags

A component may declare any number of vars and up to 64 props (past that, group some props into a struct prop).

Interaction state (state)

state declares boolean flags specifically for interaction styling. Unlike var, these are targetable from #style via :name pseudo-selectors:

tape
component Button {
    state hovered: bool = false;
    state active: bool = false;
    state disabled: bool = false;

    fn handle_pointer(ev: *ui.PointerEvent) {
        if (ev.kind == ui.PointerKind.Enter) { hovered = true; }
        if (ev.kind == ui.PointerKind.Leave) { hovered = false; }
        if (ev.kind == ui.PointerKind.Down) { active = true; ev.handled = true; }
        if (ev.kind == ui.PointerKind.Up) { active = false; }
    }
}

(ev.handled = true on the press takes the pointer: the Up is delivered only to the component that handled the Down; see ui.)

Rules:

  • state fields must be bool
  • Mutations mark dirty (same as var)
  • Targetable from #style with :name state selectors
tape
#style
Button {
    bg: #444444;
    :hovered { bg: #555555; }
    :active { bg: #333333; }
    :disabled { bg: #222222; fg: #666666; }
}

For a Button that also declares the color props bg, fg, hovered_bg, active_bg, disabled_bg and disabled_fg: bg sets a prop, and :hovered { bg: #555555; } sets the prop hovered_bg, which the component paints with while hovered is on. The compiler checks that the state and the prop exist; an unknown one is a compile error.

Props vs var vs state

propvarstate
Set byParent / #styleComponentComponent
Mutable insideNoYesYes
Marks dirtyWhen parent changesOn any mutationOn any mutation
TypeAnyAnybool only
Style-targetableYes (value override)No:name selector (planned)
Auto-setterNoYes (set_<name>)No

Last modified: