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>
14 KiB
zCatGui - Arquitectura y Decisiones de Diseño
Documento de referencia para el desarrollo de zCatGui Última actualización: 2025-12-09
1. Visión del Proyecto
1.1 Objetivo
Crear una librería GUI immediate-mode para Zig con:
- Máxima compatibilidad - Funciona en cualquier ordenador (viejo HP, nuevo Lenovo, servidor SSH)
- Software rendering - No depende de GPU/drivers
- Sistema de macros - Piedra angular: grabar y reproducir acciones
- Cross-platform - Linux, Windows, macOS
- Control total - Sin "magia" del framework, estado explícito
1.2 Filosofía
"Máxima compatibilidad, mínimas dependencias, control total del usuario"
- Simple > Complejo: microui demostró que 1,100 LOC son suficientes
- Explícito > Implícito: Estado visible, sin callbacks ocultos
- Funciona siempre: Software rendering primero, GPU opcional después
2. Paradigma: Immediate Mode
2.1 ¿Qué es Immediate Mode?
while (running) {
events = pollEvents(); // 1. Obtener input
updateState(events); // 2. TÚ actualizas estado
commands = drawUI(state); // 3. Generar comandos de dibujo
render(commands); // 4. Renderizar
}
La UI es una función pura del estado: UI = f(Estado)
2.2 Immediate vs Retained Mode
| Aspecto | Immediate (zCatGui) | Retained (Fyne) |
|---|---|---|
| Estado | Tú lo manejas | Framework lo mantiene |
| Callbacks | No hay | Muchos |
| Threading | Simple, predecible | fyne.Do() hell (402 usos) |
| Debugging | Fácil, estado visible | Difícil, estado oculto |
| Testing | Funciones puras | Mock objects complejos |
2.3 Por qué Immediate Mode
- Sin fyne.Do(): No hay threading hell
- Estado explícito: Debug simple
- Testing trivial: Funciones puras
- Control total: Sin sorpresas
3. Arquitectura de Capas
┌─────────────────────────────────────────────────────────────┐
│ Capa 4: Widgets de alto nivel │
│ (Table, Panel, Select, Modal, etc.) │
├─────────────────────────────────────────────────────────────┤
│ Capa 3: Sistema de Macros │
│ (Grabación, Reproducción, Inyección de teclas) │
├─────────────────────────────────────────────────────────────┤
│ Capa 2: Core UI │
│ (Context, Layout, Style, Input, Commands) │
├─────────────────────────────────────────────────────────────┤
│ Capa 1: Rendering │
│ (Software Rasterizer, Framebuffer, Fonts) │
├─────────────────────────────────────────────────────────────┤
│ Capa 0: Backend │
│ (SDL2 - ventanas, eventos, display) │
└─────────────────────────────────────────────────────────────┘
3.1 Capa 0: Backend (SDL2)
Responsabilidades:
- Crear/manejar ventana
- Capturar eventos (teclado, mouse)
- Mostrar framebuffer en pantalla
Por qué SDL2:
- Cross-platform probado
- Fácil de usar desde Zig
- Tiene software renderer
3.2 Capa 1: Rendering
Componentes:
Framebuffer: Array 2D de pixels RGBASoftwareRasterizer: drawRect, drawLine, drawTextFont: Bitmap fonts + TTF opcional
Approach: Command List (estilo microui)
pub const DrawCommand = union(enum) {
rect: RectCommand,
text: TextCommand,
line: LineCommand,
clip: ClipCommand,
clip_end,
};
3.3 Capa 2: Core UI
Componentes:
Context: Estado global, ID system, command listLayout: Constraints (reutilizar de zcatui)Style: Color, fonts (reutilizar de zcatui)Input: Estado de teclado/mouse
3.4 Capa 3: Sistema de Macros
Piedra angular del proyecto.
Ver sección 5 para detalles.
3.5 Capa 4: Widgets
Prioritarios (de Simifactu):
- Table (editable)
- Input (text entry)
- Select (dropdown)
- Panel (con título)
- Split (HSplit/VSplit)
- Button
- Modal
4. Decisiones de Diseño Consensuadas
4.1 Rendering: Software por Defecto
Decisión: Software rendering, GPU opcional en el futuro.
Razones:
- Funciona en cualquier ordenador
- SSH con X11 forwarding funciona
- No depende de drivers GPU
- Simple de debugear
Implementación:
Widgets → DrawCommands → Software Rasterizer → Framebuffer → SDL_Texture → Display
4.2 Backend: SDL2
Decisión: SDL2 como único backend inicial.
Razones:
- Cross-platform probado (Linux/Windows/Mac)
- API simple
- Tiene software renderer
- Fácil integración con Zig
Futuro opcional:
- X11 directo (Linux)
- Win32 directo (Windows)
4.3 Fonts: Híbrido
Decisión: Bitmap embebido + TTF opcional.
Razones:
- Bitmap siempre funciona (no dependencias)
- TTF para flexibilidad (stb_truetype)
4.4 Enfoque Híbrido para Desarrollo
Decisión: Estudiar DVUI/microui, implementar desde cero.
Razones:
- Aprender haciendo
- Control total del código
- Sin dependencias no deseadas
- Evitar fork con bagaje innecesario
4.5 Macros: Teclas Raw
Decisión: Grabar teclas literales, no comandos abstractos.
Ver sección 5.
5. Sistema de Macros
5.1 Principio Fundamental
Grabar teclas raw, reproducir teclas raw.
Usuario pulsa: Tab, Tab, Enter, "texto", Escape
Grabamos: [Tab, Tab, Enter, t, e, x, t, o, Escape]
Reproducimos: Inyectamos exactamente esas teclas
5.2 Por qué Teclas Raw (no Comandos)
| Enfoque | Pros | Contras |
|---|---|---|
| Teclas raw | Simple, mínima memoria, como Vim | Depende del estado inicial |
| Comandos abstractos | Más robusto | Complejo, más memoria |
Decisión: Teclas raw porque:
- KISS - menos código = menos bugs
- Vim lo hace así y funciona
- El estado inicial es controlable
5.3 Manejo del Ratón
Casi todo lo que hace el ratón se puede expresar como teclado:
| Acción ratón | Equivalente teclado |
|---|---|
| Click en botón | Tab hasta focus + Enter |
| Click en fila 5 | Flechas hasta fila 5 |
| Scroll down | PageDown o flechas |
| Drag splitter | Ctrl+flechas |
Estrategia:
- Fase 1: Solo teclado (macros de teclas)
- Fase 2: Mouse → traducir a teclas equivalentes
5.4 API
pub const MacroRecorder = struct {
events: ArrayList(KeyEvent),
recording: bool,
pub fn start(self: *MacroRecorder) void {
self.events.clearRetainingCapacity();
self.recording = true;
}
pub fn stop(self: *MacroRecorder) []const KeyEvent {
self.recording = false;
return self.events.items;
}
pub fn record(self: *MacroRecorder, key: KeyEvent) void {
if (self.recording) {
self.events.append(key) catch {};
}
}
pub fn save(self: *MacroRecorder, path: []const u8) !void {
// Serializar a archivo
}
pub fn load(allocator: Allocator, path: []const u8) !MacroRecorder {
// Deserializar de archivo
}
};
pub const MacroPlayer = struct {
pub fn play(
events: []const KeyEvent,
inject_fn: *const fn(KeyEvent) void,
delay_ms: u32,
) void {
for (events) |event| {
inject_fn(event);
if (delay_ms > 0) {
std.Thread.sleep(delay_ms * std.time.ns_per_ms);
}
}
}
};
5.5 Casos de Uso
- Testing automatizado: Grabar sesión → test
- Tutoriales: Macros que se ejecutan paso a paso
- Repetición: Tarea repetitiva → hotkey
- Debugging: "¿Cómo llegaste a este bug?" → envía macro
- Demos: Grabar demos que se auto-reproducen
6. Flujo de Datos
6.1 Event Loop Principal
pub fn run(app: *App) !void {
while (app.running) {
// 1. Poll eventos del backend
while (backend.pollEvent()) |raw_event| {
// 2. Traducir a KeyEvent/MouseEvent
const event = translateEvent(raw_event);
// 3. Grabar si macro activo
if (app.macro_recorder.recording) {
app.macro_recorder.record(event);
}
// 4. Actualizar estado de input
app.input.update(event);
}
// 5. Ejecutar lógica de UI (immediate mode)
app.context.beginFrame();
app.drawUI(); // Usuario define esto
app.context.endFrame();
// 6. Renderizar commands
for (app.context.commands.items) |cmd| {
app.rasterizer.execute(cmd);
}
// 7. Presentar framebuffer
backend.present(app.framebuffer);
}
}
6.2 Widget Pattern
pub fn button(ctx: *Context, label: []const u8) bool {
// 1. Obtener ID único
const id = ctx.getId(label);
// 2. Obtener bounds del layout
const bounds = ctx.layout.nextRect();
// 3. Verificar interacción
const hovered = bounds.contains(ctx.input.mousePos());
const pressed = hovered and ctx.input.mouseDown(.left);
const clicked = hovered and ctx.input.mouseReleased(.left);
// 4. Determinar estilo
const bg_color = if (pressed) pressed_color
else if (hovered) hover_color
else normal_color;
// 5. Emitir comandos de dibujo
ctx.pushCommand(.{ .rect = .{
.bounds = bounds,
.color = bg_color,
}});
ctx.pushCommand(.{ .text = .{
.pos = bounds.center(),
.text = label,
.color = text_color,
}});
// 6. Retornar si fue clickeado
return clicked;
}
7. Estructura de Archivos
zcatgui/
├── src/
│ ├── zcatgui.zig # Entry point, re-exports
│ │
│ ├── core/
│ │ ├── context.zig # Context, ID system
│ │ ├── layout.zig # Constraints (de zcatui)
│ │ ├── style.zig # Color, Style (de zcatui)
│ │ ├── input.zig # InputState
│ │ └── command.zig # DrawCommand
│ │
│ ├── widgets/
│ │ ├── button.zig
│ │ ├── label.zig
│ │ ├── input.zig
│ │ ├── select.zig
│ │ ├── table.zig # CRÍTICO
│ │ ├── panel.zig
│ │ ├── split.zig
│ │ └── modal.zig
│ │
│ ├── render/
│ │ ├── software.zig # Rasterizer
│ │ ├── framebuffer.zig # Pixel buffer
│ │ └── font.zig # Fonts
│ │
│ ├── backend/
│ │ ├── backend.zig # Interface
│ │ └── sdl2.zig # SDL2 impl
│ │
│ └── macro/
│ ├── event.zig # MacroEvent
│ ├── recorder.zig # Grabador
│ ├── player.zig # Reproductor
│ └── storage.zig # Persistencia
│
├── examples/
├── docs/
├── build.zig
└── CLAUDE.md
8. Código Reutilizable de zcatui
8.1 Layout System
Reutilizar de zcatui/src/layout.zig:
Constraint: length, min, max, percentage, ratio, fillLayout: vertical, horizontalRect: x, y, width, height, contains, intersection
8.2 Style System
Reutilizar de zcatui/src/style.zig:
Color: rgb, indexed, ANSI colorsStyle: foreground, background, modifiers
Adaptar para GUI:
- Añadir alpha channel (RGBA)
- Añadir border_radius (futuro)
- Añadir shadow (futuro)
8.3 Conceptos
- Focus management pattern
- Theme system
- Event patterns
9. Plan de Desarrollo
Fase 1: Core + Macros (1 semana)
Objetivo: Event loop funcional con grabación de macros.
- SDL2 backend (ventana, eventos)
- Framebuffer + software rasterizer (rect, text)
- Context básico
- MacroRecorder/MacroPlayer
- Button + Label (para probar)
Fase 2: Widgets Esenciales (2 semanas)
- Input (text entry)
- Select (dropdown)
- Checkbox
- List
- Layout system (de zcatui)
- Focus management
Fase 3: Widgets Avanzados (2 semanas)
- Table editable (CRÍTICO)
- Split panels
- Panel con título
- Modal/Popup
Fase 4: Pulido (1 semana)
- Themes
- TTF fonts
- Documentación
- Examples completos
10. Métricas de Éxito
10.1 Funcionales
- Funciona en Linux sin GPU dedicada
- Funciona via SSH con X11 forwarding
- Macros funcionan: grabar → reproducir
- Table editable comparable a Simifactu
10.2 Código
- Target: ~5,000-10,000 LOC total
- Tests para core y widgets
- Examples ejecutables
10.3 Performance
- 60 FPS con UI típica
- Startup < 100ms
- Memoria < 50MB
11. Referencias
Librerías Estudiadas
| Librería | Lo que tomamos |
|---|---|
| microui | Arquitectura, command list |
| DVUI | Patterns Zig, backend abstraction |
| Dear ImGui | API design, ID system |
| Gio | Constraint system |
Documentación de Investigación
docs/research/GIO_UI_ANALYSIS.mddocs/research/IMMEDIATE_MODE_LIBS.mddocs/research/SIMIFACTU_FYNE_ANALYSIS.md