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