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

tape
@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:

tape
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:

tape
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:

tape
pub fn main() -> i32 {
    let app = Counter {};
    ui.run(app, 400, 200, "Counter") or { return 1; };
    return 0;
}

In view trees:

tape
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):

tape
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/layout calls 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_mount calls — invoked automatically after instantiation and view initialization
  • View tree codegen — $init_view functions that build the child tree

Last modified: