Zylix uses a type-safe event system to handle user interactions. Events flow from platform shells through the core, triggering state changes and UI updates.
Terms#
- Event: A typed action sent from platform UI to the Zig core.
- Dispatch: The ABI call that routes events to handlers.
- Payload: Optional data attached to an event.
Concept#
Event Architecture#
sequenceDiagram
participant User
participant Shell as Platform Shell
participant Core as Zylix Core (Zig)
participant Handler as Event Handler
participant State as State Update
User->>Shell: Tap button
Note over Shell: Convert to Zylix event
Shell->>Core: zylix_dispatch(EVENT_TODO_ADD, "Buy groceries", 13)
Note over Core: Validate event type
Note over Core: Parse payload
Note over Core: Route to handler
Core->>Handler: Dispatch event
Note over Handler: switch (event) {<br/> .todo_add => addTodo(text),<br/> .todo_toggle => toggleTodo(id),<br/>}
Handler->>State: Update state
Note over State: state.todos[id].completed = true<br/>state.version += 1<br/>scheduleRender()
State-->>Core: Result code
Core-->>Shell: Return resultImplementation#
Event Types#
Built-in Events#
Zylix provides common event types for UI interactions:
pub const EventType = enum(u8) {
none = 0,
click = 1,
double_click = 2,
mouse_enter = 3,
mouse_leave = 4,
mouse_down = 5,
mouse_up = 6,
focus = 7,
blur = 8,
input = 9,
change = 10,
submit = 11,
key_down = 12,
key_up = 13,
key_press = 14,
};Application Events#
Define your own events using discriminated unions:
// events.zig
pub const Event = union(enum) {
// Counter events
counter_increment,
counter_decrement,
counter_reset,
// Todo events
todo_add: []const u8, // Payload: todo text
todo_toggle: u32, // Payload: todo ID
todo_remove: u32, // Payload: todo ID
todo_clear_completed,
todo_set_filter: Filter, // Payload: filter type
// Navigation events
navigate: Screen, // Payload: target screen
};Event Dispatch#
ABI Export#
Events are dispatched through the C ABI:
// abi.zig
export fn zylix_dispatch(
event_type: u32,
payload: ?*anyopaque,
len: usize
) c_int {
// Validate event type
if (event_type > MAX_EVENT_TYPE) {
return ERROR_INVALID_EVENT;
}
// Route to handler
const result = handleEvent(event_type, payload, len);
// Trigger render if state changed
if (result == SUCCESS and state.isDirty()) {
scheduleRender();
}
return result;
}Platform Dispatch#
Each platform calls dispatch differently:
// iOS/macOS
@_silgen_name("zylix_dispatch")
func zylix_dispatch(
_ eventType: UInt32,
_ payload: UnsafeRawPointer?,
_ len: Int
) -> Int32
// Usage
let text = "Buy groceries"
text.withCString { ptr in
zylix_dispatch(EVENT_TODO_ADD, ptr, text.count)
}// Android
external fun dispatch(eventType: Int, payload: ByteArray?, len: Int): Int
// Usage
val text = "Buy groceries".toByteArray()
ZylixLib.dispatch(EVENT_TODO_ADD, text, text.size)// Web/WASM
function dispatch(eventType, payload) {
const encoder = new TextEncoder();
const bytes = encoder.encode(payload);
const ptr = zylix.alloc(bytes.length);
zylix.memory.set(bytes, ptr);
const result = zylix.dispatch(eventType, ptr, bytes.length);
zylix.free(ptr, bytes.length);
return result;
}
// Usage
dispatch(EVENT_TODO_ADD, "Buy groceries");// Windows
[LibraryImport("zylix", EntryPoint = "zylix_dispatch")]
public static partial int Dispatch(
uint eventType,
IntPtr payload,
nuint len
);
// Usage
var text = "Buy groceries"u8.ToArray();
fixed (byte* ptr = text) {
ZylixInterop.Dispatch(EVENT_TODO_ADD, (IntPtr)ptr, (nuint)text.Length);
}// Linux (GTK)
extern int zylix_dispatch(
uint32_t event_type,
void* payload,
size_t len
);
// Usage
const char* text = "Buy groceries";
zylix_dispatch(EVENT_TODO_ADD, (void*)text, strlen(text));Event Handlers#
Handler Registration#
// Callback ID constants
pub const CALLBACK_INCREMENT = 1;
pub const CALLBACK_DECREMENT = 2;
pub const CALLBACK_RESET = 3;
pub const CALLBACK_ADD_TODO = 10;
pub const CALLBACK_TOGGLE_TODO = 11;
pub const CALLBACK_REMOVE_TODO = 12;
// Handler dispatch
pub fn handleCallback(id: u32, data: ?*anyopaque) void {
switch (id) {
CALLBACK_INCREMENT => state.handleIncrement(),
CALLBACK_DECREMENT => state.handleDecrement(),
CALLBACK_RESET => state.handleReset(),
CALLBACK_ADD_TODO => {
if (data) |ptr| {
const text = @as([*:0]const u8, @ptrCast(ptr));
todo.addTodo(std.mem.sliceTo(text, 0));
}
},
CALLBACK_TOGGLE_TODO => {
if (data) |ptr| {
const id = @as(*const u32, @ptrCast(@alignCast(ptr))).*;
todo.toggleTodo(id);
}
},
else => {},
}
}Event Handler Structure#
pub const EventHandler = struct {
event_type: EventType = .none,
callback_id: u32 = 0,
prevent_default: bool = false,
stop_propagation: bool = false,
};Handling Specific Events#
Click Events#
fn handleClick(callback_id: u32) void {
switch (callback_id) {
CALLBACK_INCREMENT => {
const app = state.getStore().getStateMut();
app.counter += 1;
state.getStore().commit();
},
CALLBACK_SUBMIT => {
submitForm();
},
else => {},
}
}Input Events#
fn handleInput(callback_id: u32, text: []const u8) void {
switch (callback_id) {
CALLBACK_TEXT_INPUT => {
const app = state.getStore().getStateMut();
const len = @min(text.len, app.input_text.len - 1);
@memcpy(app.input_text[0..len], text[0..len]);
app.input_len = len;
state.getStore().commit();
},
CALLBACK_SEARCH => {
performSearch(text);
},
else => {},
}
}Keyboard Events#
pub const KeyEvent = struct {
key_code: u16,
modifiers: KeyModifiers,
};
pub const KeyModifiers = packed struct {
shift: bool = false,
ctrl: bool = false,
alt: bool = false,
meta: bool = false,
};
fn handleKeyDown(event: KeyEvent) void {
// Enter key
if (event.key_code == 13) {
if (state.getInput().len > 0) {
todo.addTodo(state.getInput());
state.clearInput();
}
}
// Escape key
if (event.key_code == 27) {
state.clearInput();
}
// Ctrl+Z - Undo
if (event.key_code == 90 and event.modifiers.ctrl) {
state.undo();
}
}Event Validation#
Type Validation#
fn validateEvent(event_type: u32) !EventType {
if (event_type > @intFromEnum(EventType.key_press)) {
return error.InvalidEventType;
}
return @enumFromInt(event_type);
}Payload Validation#
fn validatePayload(
event_type: EventType,
payload: ?*anyopaque,
len: usize
) !void {
switch (event_type) {
.todo_add => {
if (payload == null or len == 0) {
return error.MissingPayload;
}
if (len > MAX_TODO_TEXT_LEN) {
return error.PayloadTooLarge;
}
},
.todo_toggle, .todo_remove => {
if (len != @sizeOf(u32)) {
return error.InvalidPayloadSize;
}
},
else => {},
}
}Event Queue#
For batching multiple events:
pub const EventQueue = struct {
events: [MAX_QUEUED_EVENTS]QueuedEvent = undefined,
count: usize = 0,
pub fn push(self: *EventQueue, event: QueuedEvent) !void {
if (self.count >= MAX_QUEUED_EVENTS) {
return error.QueueFull;
}
self.events[self.count] = event;
self.count += 1;
}
pub fn processAll(self: *EventQueue) void {
for (self.events[0..self.count]) |event| {
processEvent(event);
}
self.count = 0;
}
};Error Handling#
Result Codes#
pub const EventResult = enum(c_int) {
success = 0,
error_invalid_event = -1,
error_invalid_payload = -2,
error_handler_failed = -3,
error_state_locked = -4,
};Error Recovery#
fn dispatchWithRecovery(
event_type: u32,
payload: ?*anyopaque,
len: usize
) EventResult {
// Validate first
const event = validateEvent(event_type) catch {
return .error_invalid_event;
};
validatePayload(event, payload, len) catch {
return .error_invalid_payload;
};
// Try to handle
handleEvent(event, payload, len) catch |err| {
// Log error
std.log.err("Event handler failed: {}", .{err});
// Attempt recovery
state.rollback();
return .error_handler_failed;
};
return .success;
}Best Practices#
1. Keep Events Granular#
// Good: Specific events
pub const Event = union(enum) {
todo_add: []const u8,
todo_toggle: u32,
todo_remove: u32,
todo_edit: struct { id: u32, text: []const u8 },
};
// Avoid: Generic events
pub const Event = union(enum) {
todo_action: struct {
action_type: u8,
id: ?u32,
text: ?[]const u8,
},
};2. Use Constants for Callback IDs#
// Good: Named constants
pub const CALLBACK_INCREMENT = 1;
pub const CALLBACK_DECREMENT = 2;
node.props.on_click = CALLBACK_INCREMENT;
// Avoid: Magic numbers
node.props.on_click = 1;3. Validate Before Processing#
// Good: Validate first
fn handleTodoAdd(text: []const u8) !void {
if (text.len == 0) return error.EmptyText;
if (text.len > MAX_TEXT_LEN) return error.TextTooLong;
if (todo_count >= MAX_TODOS) return error.TooManyTodos;
// Safe to add
addTodo(text);
}4. Batch Related Events#
// Good: Batch when possible
fn handleBulkComplete(ids: []const u32) void {
for (ids) |id| {
completeTodo(id);
}
// Single commit after all changes
state.commit();
scheduleRender();
}Debugging Events#
Logging#
fn logEvent(event_type: EventType, payload: ?*anyopaque) void {
std.log.debug(
"Event: {s}, payload: {}",
.{ @tagName(event_type), payload != null }
);
}Event History#
var event_history: [256]EventRecord = undefined;
var history_index: usize = 0;
fn recordEvent(event: Event) void {
event_history[history_index] = .{
.event = event,
.timestamp = std.time.milliTimestamp(),
};
history_index = (history_index + 1) % 256;
}Pitfalls#
- Dispatching before initialization returns an error code.
- Payload length mismatches lead to invalid reads.
- Reusing payload buffers after dispatch can corrupt data.
Implementation Links#
Samples#
Next Steps#
- State Management - How events trigger state changes
- Components - Attach event handlers to components
- Virtual DOM - How events trigger re-renders