Lifecycle & Rendering

Lifecycle: on_mount and on_unmount

When a component is inserted into the view tree, the compiler calls its on_mount method (if defined). When removed from the tree, on_unmount is called:

tape
component Connection {
    var conn: ?net.TcpStream = null;

    fn on_mount() {
        conn = net.connect_host("localhost", 8080) or { return; };
    }

    fn on_unmount() {
        if (conn != null) {
            var s: net.TcpStream = conn!;
            s.close();
            conn = null;
        }
    }
}
  • on_mount — called once after the component is added to the active view tree
  • on_unmount — called once before the component is removed from the view tree

Both are optional. Define either, both, or neither.

After on_unmount and the children’s unmounting, the component’s own strings and other managed vars and props are freed (and left empty), anything the UI still points at it for (keyboard focus, pointer capture, hover, an open dialog) is cleared, and the component’s memory is freed: mounting and unmounting a tree, or switching an if branch and updating a for list, gives back everything it allocated.

A component made in #code with let c = Timer { ... }; is destroyed the same way with ui.unmount(c). @drop(c) only frees its managed vars and props: it does not call on_unmount, and the component itself stays allocated.

After a hot reload: on_reload

In a hot run (tape run --hot), when a reload replaces a component’s code, on_reload runs once on each of its instances, after their vars were carried over to the new code. It re-derives what depended on the old code: a coroutine that had to restart reads started() == false, so on_reload can start it again.

tape
component Guard {
    var walk: walk_route;
    co fn walk_route() { ... }

    fn on_reload() {
        if (!walk.started()) { walk = walk_route.start(); }
    }
}

Rendering via @dispatch

Visual rendering is not a special compiler feature — it uses the standard dispatch mechanism. Components opt into painting by calling ui.drawable() in a @tape block:

tape
component Panel {
    @tape { ui.drawable(); }

    prop bg: color;

    fn paint(s: *gfx.Surface, x: i64, y: i64, w: i64, h: i64) {
        s.fill_rect(x, y, w, h, bg);
    }
}

ui.drawable() is a comptime function that emits:

tape
pub @tape fn drawable() {
    @emit var bounds_x: i64 = 0;
    @emit var bounds_y: i64 = 0;
    @emit var bounds_w: i64 = 0;
    @emit var bounds_h: i64 = 0;
    @emit @dispatch fn paint(s: *gfx.Surface, x: i64, y: i64, w: i64, h: i64);
}

Layout dispatch

Components that manage child positioning opt into layout:

tape
component MyLayout {
    @tape { ui.drawable(); }
    @tape { ui.layoutable(); }

    fn layout(x: i64, y: i64, w: i64, h: i64) {
        // position children within bounds
    }

    fn paint(s: *gfx.Surface, x: i64, y: i64, w: i64, h: i64) {
        // draw background, then the children: ui.paint_tree(child, s)
    }
}

ui.layoutable() emits @dispatch fn layout(...). A component that is both drawable and layoutable owns its subtree: layout places each child (ui.layout_tree(child, x, y, w, h)) and paint paints them (ui.paint_tree(child, s)) after its own drawing; the default tree walker does neither for it (ui: the tree).

Interaction dispatch

Components that handle input opt into the interactive protocol:

tape
component Button {
    @tape { ui.drawable(); }
    @tape { ui.interactive(); }

    event on_click();

    fn handle_pointer(ev: *ui.PointerEvent) {
        if (ev.kind == ui.PointerKind.Down) { ev.handled = true; }  // take the press
        if (ev.kind == ui.PointerKind.Up) {
            fire on_click();
        }
    }

    fn handle_key(ev: ui.KeyEvent) {
        // keyboard handling
    }
}

ui.interactive() emits @dispatch fn handle_pointer(ev: *PointerEvent) and @dispatch fn handle_key(ev: KeyEvent).

Rendering cadence

  1. Input event arrives (pointer, key, timer)
  2. Event handler executes (may mutate var/state multiple times)
  3. Handler returns — dirty flags are set
  4. Frame tick: runtime walks dirty components
  5. For each dirty component: recalculate layout if needed, repaint dirty region
  6. Clear dirty flags

A component is repainted at most once per frame, regardless of how many state mutations occurred, and only when something changed: the frame repaints the dirty components’ rectangles (with whatever lies under and over them there) and a window shows just those (ui: what gets repainted).

Dispatch summary

CapabilityComptime helperDispatch methods
Paintingui.drawable()paint(s, x, y, w, h)
Layoutui.layoutable()layout(x, y, w, h)
Inputui.interactive()handle_pointer(ev), handle_key(ev)

Last modified: