Props & State
Props
Props are typed inputs set by the parent. They are immutable within the component.
component Display {
prop value: string = "";
prop size: i64 = 16;
prop bold: bool = false;
}The parent sets props at instantiation:
Display { value: "Hello"; size: 24; }
Display { value: count_text; } // uses default size and boldRules:
- 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
forlist or#codealike. Defaults run in order, after the parent’s values and before the component’svarinitializers, so avarcan 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
stringprop 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, andforvariables - Props are style-targetable —
#stylecan override default values
Props and #style
Props can be set from #style blocks, overriding defaults without the parent explicitly passing them:
component Button {
prop bg: color = #444444;
prop fg: color = #FFFFFF;
prop label: string = "";
}#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:
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:
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:
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:
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:
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):
- Event handler executes (may mutate state multiple times)
- Handler returns
- Frame tick: runtime walks dirty components, repaints dirty regions
- 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:
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:
statefields must bebool- Mutations mark dirty (same as
var) - Targetable from
#stylewith:namestate selectors
#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
prop | var | state | |
|---|---|---|---|
| Set by | Parent / #style | Component | Component |
| Mutable inside | No | Yes | Yes |
| Marks dirty | When parent changes | On any mutation | On any mutation |
| Type | Any | Any | bool only |
| Style-targetable | Yes (value override) | No | :name selector (planned) |
| Auto-setter | No | Yes (set_<name>) | No |
Last modified: