Events

Declaring events

Events declare typed callback slots that the parent binds at instantiation:

tape
component Button {
    prop label: string = "";
    event on_click();
    event on_hover(x: i64, y: i64);
}

Events can have up to 8 parameters.

Firing events

Use the fire keyword inside a component method to invoke the bound handler:

tape
component Button {
    prop label: string = "";
    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();
        }
    }
}

(The release, Up, is delivered to the component that handled the Down; see ui.)

If no handler is bound, fire is a no-op (null-guarded function pointer call): its arguments are not evaluated either. When a handler is bound, each argument is evaluated once.

Binding handlers (parent side)

The parent binds a function to the event by name:

tape
fn save_file() {
    io.println("saving...");
}

component Toolbar {
    view {
        widgets.Button { label: "Save"; on_click: save_file; }
    }
}

The handler must match the event exactly: the same parameter count, each parameter of the same type, and no return value. A component method can be a handler too (on_click: increment; inside a component’s view): it is bound with its instance, and that hidden instance pointer is not part of the match. A mismatch is a compile error naming both signatures:

text
event handler mismatch: 'set_name' is 'fn(string)', but event 'on_change' needs 'fn(i64)' (parameter types differ)

fire is checked the same way: fire on_delete("x") on event on_delete(index: i64) is an argument type error, and so is a wrong argument count.

Non-visual #code components bind events the same way when they are instantiated in code:

tape
fn tick(n: i64) { /* ... */ }

let t = Timer { interval: 1000; on_tick: tick; };

A handler can be a pub function of an imported module, named through the module: Timer { interval: 1000; on_tick: sched.tick; }.

A prop of function type (prop format: fn(i64) -> string;) is a plain function pointer, not an event: bind a module function (f or m.f) of exactly that type to it and call it as format(n).

An event is the only place a component’s method goes as a value. Given where a plain function pointer is expected (an argument such as ui.watch(h, tick), a fn-typed variable, field, array element, return value or prop) it would lose its instance, so it is a compile error:

text
'tick' is a method of component Ticker: it needs its instance, which a plain fn value doesn't carry; bind it to an event instead (e.g. ui.Watch { handle: h; on_ready: tick; })

For the loop to call a component back on a handle or on time, put a ui.Watch or ui.Timer in its view and bind the method to its event.

Passing data through events

Declare parameters in the event signature to carry data to the parent:

tape
component TodoItem {
    prop index: i64;
    event on_delete(index: i64);

    fn handle_click() {
        fire on_delete(index);
    }
}

Parent side:

tape
fn delete_item(index: i64) { /* ... */ }
TodoItem { index: 3; on_delete: delete_item; }

Auto-setters as event handlers

Generated set_<var> methods are commonly bound to change events:

tape
component Volume {
    var level: i64 = 50;

    view {
        widgets.Slider { min_val: 0; max_val: 100; value: level; on_change: set_level; }
    }
}

Design: no closures

Events are single function pointers — not closures, not listener lists. This means:

  • No hidden heap allocations
  • No capture analysis
  • No multi-subscriber patterns (use explicit fan-out if needed)
  • Predictable, zero-overhead dispatch (null check + indirect call)

Last modified: