Component Overview
Components are built into the language. The compiler understands them at the IR level and generates vtable dispatch, dirty marking, auto-setters, and efficient state updates — no runtime framework overhead.
What is a component?
A component is a self-contained unit with:
- Props — typed inputs from the parent (immutable inside)
- Vars — mutable reactive state (mutations mark dirty)
- State — boolean interaction flags (targetable from
#style) - Events — typed single-handler callbacks
- Slots — named insertion points for child content
- Dispatch — vtable slots for polymorphic method calls
- Methods — regular functions
- View — a declarative tree of child components
A simple component
@profile(t2);
import io from "io";
import ui from "ui";
import widgets from "widgets";
component Counter {
var count: i64 = 0;
var count_text: string = "0";
fn increment() {
count = count + 1;
count_text = "{count}";
}
fn decrement() {
count = count - 1;
count_text = "{count}";
}
view {
ui.Column {
widgets.Label { text: count_text; fg: #333333; size: 24; }
ui.Row {
widgets.Button { label: "+"; on_click: increment; }
widgets.Button { label: "-"; on_click: decrement; }
}
}
}
}
pub fn main() -> i32 {
let app = Counter {};
ui.run(app, 400, 200, "Counter") or { return 1; };
return 0;
}Visual vs non-visual
Components don’t have to be visual. A non-visual component manages state and events without a view tree:
component Timer {
prop interval: u64;
var running: bool = false;
event on_tick();
fn on_mount() {
running = true;
}
}Visual components add a view { } block and typically use @tape { ui.drawable(); } to opt into the paint dispatch:
component Badge {
@tape { ui.drawable(); }
prop label: string = "";
prop bg: color = #333333;
fn paint(s: *gfx.Surface, x: i64, y: i64, w: i64, h: i64) {
s.fill_rect(x, y, w, h, bg);
s.draw_text(x + 4, y + 12, label, #FFFFFF, 14);
}
}Instantiation
Components are instantiated with struct-literal-like syntax, binding props and events:
pub fn main() -> i32 {
let app = Counter {};
ui.run(app, 400, 200, "Counter") or { return 1; };
return 0;
}In view trees:
view {
widgets.Button { label: "+"; on_click: increment; }
}Components as values
An instance is a reference: a parameter, local or global whose type names a component (Counter, window.Settings) refers to the same instance, so a function can read its vars and props, call its methods and assign its vars (which marks it dirty like an assignment inside it does):
fn reset(c: Counter) {
c.count = 0;
}
let app = Counter {};
reset(app);*Counter is the same type as Counter (the spelling a setter’s or method’s function type uses, fn(*Counter, i64)): the instance is a reference already.
For now an instance is not kept inside data: a struct field, a tagged union’s payload, an array or slice element ([4]Counter) and an optional (?Counter, ?*Counter) of a component type are compile errors. Instances live at module level and in the view tree.
Why built-in?
The compiler generates:
- Vtable dispatch — polymorphic
paint/handle_pointer/layoutcalls through function pointer tables - Dirty marking — bit-per-var flags set on mutation, checked once per frame
- Auto-setters —
set_<var>methods generated unless manually defined on_mountcalls — invoked automatically after instantiation and view initialization- View tree codegen —
$init_viewfunctions that build the child tree
Last modified: