Events
Declaring events
Events declare typed callback slots that the parent binds at instantiation:
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:
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:
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:
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:
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:
'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:
component TodoItem {
prop index: i64;
event on_delete(index: i64);
fn handle_click() {
fire on_delete(index);
}
}Parent side:
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:
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: