Event System
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.
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 result
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;
}Next Steps
- State Management - How events trigger state changes
- Components - Attach event handlers to components
- Virtual DOM - How events trigger re-renders