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 str from "str";
import widgets from "widgets";

component Counter {
    var count: i64 = 0;
    var count_text: *u8 = "0";

    fn increment() {
        count = count + 1;
        count_text = str.i64_to_cstr(count);
    }

    fn decrement() {
        count = count - 1;
        count_text = str.i64_to_cstr(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 as *u8, 400, 200, "Counter");
    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: *u8;
    prop bg: color = #333333;

    fn paint(ctx: *u8, x: i32, y: i32, w: i32, h: i32) {
        gfx.fill_rect(ctx, x, y, w, h, bg);
        gfx.draw_text_utf8(ctx, null, null, 14, x + 4, y + 12, label, #FFFFFF);
    }
}

Instantiation

Components are instantiated with struct-literal-like syntax, binding props and events:

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

In view trees:

tape
view {
    widgets.Button { label: "+"; on_click: increment; }
}

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