Immediate Mode GUI library for Zig with software rendering. Core features: - SDL2 backend for cross-platform window/events - Software rasterizer (works everywhere, including SSH) - Macro recording system (cornerstone feature, like Vim) - Command-list rendering (DrawRect, DrawText, etc.) - Layout system with constraints - Color/Style system with themes Project structure: - src/core/: context, command, input, layout, style - src/macro/: MacroRecorder, MacroPlayer, MacroStorage - src/render/: Framebuffer, SoftwareRenderer, Font - src/backend/: Backend interface, SDL2 implementation - examples/: hello.zig, macro_demo.zig - docs/: Architecture, research (Gio, immediate-mode libs, Simifactu) Build: zig build (requires SDL2-devel) Tests: 16 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
176 lines
6.1 KiB
Zig
176 lines
6.1 KiB
Zig
//! Macro Demo - Demonstrates the macro recording system
|
|
//!
|
|
//! Press:
|
|
//! - R: Start/stop recording
|
|
//! - P: Play back recorded macro
|
|
//! - S: Save macro to file
|
|
//! - L: Load macro from file
|
|
//! - ESC: Quit
|
|
//!
|
|
//! Type any keys while recording to capture them.
|
|
|
|
const std = @import("std");
|
|
const zcatgui = @import("zcatgui");
|
|
|
|
const Framebuffer = zcatgui.render.Framebuffer;
|
|
const SoftwareRenderer = zcatgui.render.SoftwareRenderer;
|
|
const Sdl2Backend = zcatgui.backend.Sdl2Backend;
|
|
const MacroRecorder = zcatgui.MacroRecorder;
|
|
const MacroPlayer = zcatgui.MacroPlayer;
|
|
const Color = zcatgui.Color;
|
|
const Command = zcatgui.Command;
|
|
const KeyEvent = zcatgui.KeyEvent;
|
|
|
|
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
// Initialize backend
|
|
var backend = try Sdl2Backend.init("zCatGui - Macro Demo", 800, 600);
|
|
defer backend.deinit();
|
|
|
|
// Create framebuffer
|
|
var fb = try Framebuffer.init(allocator, 800, 600);
|
|
defer fb.deinit();
|
|
|
|
// Create renderer
|
|
var renderer = SoftwareRenderer.init(&fb);
|
|
|
|
// Create macro recorder
|
|
var recorder = MacroRecorder.init(allocator);
|
|
defer recorder.deinit();
|
|
|
|
// State
|
|
var running = true;
|
|
var last_keys: [10]KeyEvent = undefined;
|
|
var last_key_count: usize = 0;
|
|
var status_message: []const u8 = "Press R to start recording";
|
|
|
|
// Event injection function for playback
|
|
const inject = struct {
|
|
var injected_count: usize = 0;
|
|
|
|
fn injectKey(key: KeyEvent) void {
|
|
_ = key;
|
|
injected_count += 1;
|
|
std.debug.print("Injected key #{d}\n", .{injected_count});
|
|
}
|
|
}.injectKey;
|
|
|
|
while (running) {
|
|
// Poll events
|
|
while (backend.pollEvent()) |event| {
|
|
switch (event) {
|
|
.quit => running = false,
|
|
|
|
.key => |key| {
|
|
if (!key.pressed) continue;
|
|
|
|
// Record if recording
|
|
recorder.record(key);
|
|
|
|
// Store for display
|
|
if (last_key_count < last_keys.len) {
|
|
last_keys[last_key_count] = key;
|
|
last_key_count += 1;
|
|
} else {
|
|
// Shift left
|
|
for (0..last_keys.len - 1) |i| {
|
|
last_keys[i] = last_keys[i + 1];
|
|
}
|
|
last_keys[last_keys.len - 1] = key;
|
|
}
|
|
|
|
// Handle special keys
|
|
switch (key.key) {
|
|
.escape => running = false,
|
|
|
|
.r => {
|
|
if (recorder.isRecording()) {
|
|
_ = recorder.stop();
|
|
status_message = "Recording stopped";
|
|
std.debug.print("Stopped recording. {d} events captured.\n", .{recorder.eventCount()});
|
|
} else {
|
|
recorder.start();
|
|
status_message = "Recording...";
|
|
std.debug.print("Started recording.\n", .{});
|
|
}
|
|
},
|
|
|
|
.p => {
|
|
if (!recorder.isRecording() and recorder.eventCount() > 0) {
|
|
status_message = "Playing back...";
|
|
std.debug.print("Playing back {d} events.\n", .{recorder.eventCount()});
|
|
MacroPlayer.play(recorder.stop(), inject);
|
|
status_message = "Playback complete";
|
|
}
|
|
},
|
|
|
|
.s => {
|
|
if (recorder.eventCount() > 0) {
|
|
recorder.save("macro.zcm") catch |err| {
|
|
std.debug.print("Save failed: {}\n", .{err});
|
|
};
|
|
status_message = "Saved to macro.zcm";
|
|
std.debug.print("Saved macro to macro.zcm\n", .{});
|
|
}
|
|
},
|
|
|
|
.l => {
|
|
recorder.load("macro.zcm") catch |err| {
|
|
std.debug.print("Load failed: {}\n", .{err});
|
|
};
|
|
status_message = "Loaded from macro.zcm";
|
|
std.debug.print("Loaded macro: {d} events\n", .{recorder.eventCount()});
|
|
},
|
|
|
|
else => {},
|
|
}
|
|
},
|
|
|
|
.resize => |size| {
|
|
try fb.resize(size.width, size.height);
|
|
},
|
|
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
// Clear
|
|
renderer.clear(Color.background);
|
|
|
|
// Draw title area
|
|
renderer.execute(Command.rect(0, 0, fb.width, 50, Color.rgb(40, 40, 40)));
|
|
|
|
// Draw status
|
|
const status_color = if (recorder.isRecording()) Color.danger else Color.foreground;
|
|
renderer.execute(Command.rect(10, 60, 200, 20, status_color));
|
|
|
|
// Draw event count
|
|
renderer.execute(Command.rect(10, 90, @intCast(recorder.eventCount() * 10), 20, Color.primary));
|
|
|
|
// Draw last keys as blocks
|
|
for (0..last_key_count) |i| {
|
|
const x: i32 = @intCast(10 + i * 50);
|
|
renderer.execute(Command.rect(x, 150, 40, 40, Color.secondary));
|
|
}
|
|
|
|
// Draw instructions area
|
|
renderer.execute(Command.rect(10, 250, 400, 120, Color.rgb(35, 35, 35)));
|
|
// Text would go here with a proper font
|
|
|
|
// Recording indicator
|
|
if (recorder.isRecording()) {
|
|
renderer.execute(Command.rect(@as(i32, @intCast(fb.width)) - 30, 10, 20, 20, Color.danger));
|
|
}
|
|
|
|
// Present
|
|
backend.present(&fb);
|
|
|
|
// Cap at ~60 FPS
|
|
std.Thread.sleep(16 * std.time.ns_per_ms);
|
|
}
|
|
|
|
std.debug.print("Final status: {s}\n", .{status_message});
|
|
}
|