feat: zCatGui v0.1.0 - Initial project setup

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>
This commit is contained in:
reugenio 2025-12-09 01:30:05 +01:00
commit 59c597fc18
22 changed files with 5752 additions and 0 deletions

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
# Zig
.zig-cache/
zig-out/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

535
CLAUDE.md Normal file
View file

@ -0,0 +1,535 @@
# zCatGui - GUI Library para Zig
> **IMPORTANTE PARA CLAUDE**: Lee la sección "PROTOCOLO DE INICIO" antes de hacer cualquier cosa.
---
## PROTOCOLO DE INICIO (LEER PRIMERO)
### Paso 1: Leer normas del equipo
```
/mnt/cello2/arno/re/recode/TEAM_STANDARDS/LAST_UPDATE.md
```
### Paso 2: Leer normas completas si es necesario
```
/mnt/cello2/arno/re/recode/TEAM_STANDARDS/NORMAS_TRABAJO_CONSENSUADAS.md
/mnt/cello2/arno/re/recode/TEAM_STANDARDS/QUICK_REFERENCE.md
```
### Paso 3: Leer documentación de investigación
```
docs/research/GIO_UI_ANALYSIS.md # Análisis de Gio UI (Go)
docs/research/IMMEDIATE_MODE_LIBS.md # Comparativa librerías immediate-mode
docs/research/SIMIFACTU_FYNE_ANALYSIS.md # Requisitos extraídos de Simifactu
docs/ARCHITECTURE.md # Arquitectura y decisiones de diseño
```
### Paso 4: Verificar estado del proyecto
```bash
cd /mnt/cello2/arno/re/recode/zig/zcatgui
git status
git log --oneline -3
zig build test
```
### Paso 5: Continuar trabajo
Una vez verificado el estado, continúa desde donde se dejó.
---
## INFORMACIÓN DEL PROYECTO
| Campo | Valor |
|-------|-------|
| **Nombre** | zCatGui |
| **Versión** | v0.1.0 - EN DESARROLLO |
| **Fecha inicio** | 2025-12-09 |
| **Lenguaje** | Zig 0.15.2 |
| **Paradigma** | Immediate Mode GUI |
| **Inspiración** | Gio (Go), microui (C), DVUI (Zig), Dear ImGui (C++) |
| **Proyecto hermano** | zcatui (TUI library) |
### Descripción
**zCatGui** es una librería GUI immediate-mode para Zig con las siguientes características:
1. **Software Rendering por defecto** - Funciona en cualquier ordenador sin GPU
2. **Cross-platform** - Linux, Windows, macOS
3. **SSH compatible** - Funciona via X11 forwarding
4. **Sistema de Macros** - Grabación/reproducción de acciones (piedra angular)
5. **Sin dependencias pesadas** - Solo SDL2 para ventanas
### Filosofía
> "Máxima compatibilidad, mínimas dependencias, control total del usuario"
- Funciona en cualquier ordenador (viejo HP, nuevo Lenovo, servidor SSH)
- Software rendering primero, GPU opcional después
- Sistema de macros integrado desde el diseño
- Immediate mode = estado explícito, sin threading hell
---
## RUTAS IMPORTANTES
```bash
# Este proyecto
/mnt/cello2/arno/re/recode/zig/zcatgui/
# Proyecto hermano (TUI)
/mnt/cello2/arno/re/recode/zig/zcatui/
# Proyecto de referencia (usa Fyne, queremos replicar funcionalidad)
/mnt/cello2/arno/re/recode/go/simifactu/
# Normas del equipo
/mnt/cello2/arno/re/recode/TEAM_STANDARDS/
# Compilador Zig 0.15.2
/mnt/cello2/arno/re/recode/zig/zig-0.15.2/zig-x86_64-linux-0.15.2/zig
```
---
## COMANDOS FRECUENTES
```bash
# Compilar
zig build
# Tests
zig build test
# Ejemplos (cuando estén implementados)
zig build hello
zig build button-demo
zig build macro-demo
# Git
git status
git add -A && git commit -m "mensaje"
git push
```
---
## ARQUITECTURA
### Paradigma: Immediate Mode
```
┌─────────────────────────────────────────────────────────────┐
│ IMMEDIATE MODE │
├─────────────────────────────────────────────────────────────┤
│ while (running) { │
│ events = pollEvents(); // Input │
│ updateState(events); // TÚ manejas estado │
│ commands = drawUI(state); // Genera comandos │
│ render(commands); // Dibuja │
│ } │
└─────────────────────────────────────────────────────────────┘
vs Retained Mode (Fyne):
- Framework mantiene árbol de widgets
- Callbacks para cambios
- fyne.Do() para threading
- Estado oculto, sincronización compleja
```
### Capas de la Librería
```
┌─────────────────────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────────────────────┘
```
### Estructura de Archivos
```
zcatgui/
├── src/
│ ├── zcatgui.zig # Entry point, re-exports
│ │
│ ├── core/
│ │ ├── context.zig # Context, ID system, state pool
│ │ ├── layout.zig # Constraints, Flex (de zcatui)
│ │ ├── style.zig # Color, Style (de zcatui)
│ │ ├── input.zig # InputState, KeyEvent, MouseEvent
│ │ └── command.zig # DrawCommand list
│ │
│ ├── widgets/
│ │ ├── button.zig
│ │ ├── label.zig
│ │ ├── input.zig # Text entry
│ │ ├── select.zig # Dropdown
│ │ ├── checkbox.zig
│ │ ├── slider.zig
│ │ ├── table.zig # Editable table (CRÍTICO)
│ │ ├── list.zig
│ │ ├── panel.zig # Window-like container
│ │ ├── split.zig # HSplit/VSplit
│ │ ├── popup.zig
│ │ └── modal.zig
│ │
│ ├── render/
│ │ ├── software.zig # Software rasterizer
│ │ ├── framebuffer.zig # Pixel buffer RGBA
│ │ └── font.zig # Bitmap + TTF fonts
│ │
│ ├── backend/
│ │ ├── backend.zig # Backend interface
│ │ └── sdl2.zig # SDL2 implementation
│ │
│ └── macro/
│ ├── event.zig # MacroEvent (teclas raw)
│ ├── recorder.zig # Grabador
│ ├── player.zig # Reproductor
│ └── storage.zig # Guardar/cargar macros
├── examples/
│ ├── hello.zig
│ ├── button_demo.zig
│ └── macro_demo.zig
├── docs/
│ ├── ARCHITECTURE.md # Arquitectura detallada
│ └── research/
│ ├── GIO_UI_ANALYSIS.md
│ ├── IMMEDIATE_MODE_LIBS.md
│ └── SIMIFACTU_FYNE_ANALYSIS.md
├── build.zig
├── build.zig.zon
├── README.md
└── CLAUDE.md # Este archivo
```
---
## SISTEMA DE MACROS (PIEDRA ANGULAR)
### Concepto
El sistema de macros permite **grabar y reproducir** todas las acciones del usuario.
**Principio**: Grabar teclas raw, no comandos abstractos.
```
Usuario pulsa: Tab, Tab, Enter, "texto", Escape
Grabamos: [Tab, Tab, Enter, t, e, x, t, o, Escape]
Reproducimos: Inyectamos exactamente esas teclas
```
### Por qué teclas raw (no comandos)
| Enfoque | Pros | Contras |
|---------|------|---------|
| **Teclas raw** | Simple, mínima memoria, reproducción exacta | Depende del estado inicial |
| Comandos semánticos | Más robusto | Complejo, más memoria, traducción bidireccional |
**Decisión**: Teclas raw (como Vim). Razones:
1. KISS - menos código = menos bugs
2. Vim lo hace así y funciona
3. El estado inicial es controlable
### 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**:
1. Fase 1: Solo teclado (macros de teclas)
2. Fase 2: Mouse → traducir a teclas equivalentes
### API Propuesta
```zig
pub const MacroRecorder = struct {
events: ArrayList(KeyEvent),
recording: bool,
pub fn start(self: *MacroRecorder) void;
pub fn stop(self: *MacroRecorder) []const KeyEvent;
pub fn record(self: *MacroRecorder, key: KeyEvent) void;
pub fn save(self: *MacroRecorder, path: []const u8) !void;
pub fn load(allocator: Allocator, path: []const u8) !MacroRecorder;
};
pub const MacroPlayer = struct {
pub fn play(
events: []const KeyEvent,
inject_fn: *const fn(KeyEvent) void,
delay_ms: u32,
) void;
};
```
### Casos de Uso
1. **Testing automatizado**: Grabar sesión → convertir en test
2. **Tutoriales**: Macros que se ejecutan paso a paso
3. **Repetición**: Grabar tarea repetitiva, asignar a hotkey
4. **Debugging**: "¿Cómo llegaste a este bug?" → envía el macro
5. **Demos**: Grabar demos que se reproducen en la app
---
## WIDGETS PRIORITARIOS
Basado en análisis de Simifactu (ver `docs/research/SIMIFACTU_FYNE_ANALYSIS.md`):
| # | Widget | Prioridad | Descripción |
|---|--------|-----------|-------------|
| 1 | **Table** | CRÍTICA | Edición in-situ, navegación teclado, dirty tracking |
| 2 | **Input** | CRÍTICA | Text entry con validación |
| 3 | **Select** | CRÍTICA | Dropdown selection |
| 4 | **Panel** | ALTA | Container con título y bordes |
| 5 | **Split** | ALTA | HSplit/VSplit draggable |
| 6 | **Button** | ALTA | Con estados disabled, importance |
| 7 | **Modal** | MEDIA | Diálogos modales |
| 8 | **List** | MEDIA | Lista seleccionable |
| 9 | **Checkbox** | MEDIA | Toggle boolean |
| 10 | **Label** | BAJA | Texto estático |
---
## RENDERING
### Software Rasterizer (Command List approach)
```zig
pub const DrawCommand = union(enum) {
rect: struct {
x: i32,
y: i32,
w: u32,
h: u32,
color: Color,
},
text: struct {
x: i32,
y: i32,
text: []const u8,
color: Color,
font: *Font,
},
line: struct {
x1: i32,
y1: i32,
x2: i32,
y2: i32,
color: Color,
},
clip: struct {
x: i32,
y: i32,
w: u32,
h: u32,
},
clip_end,
};
```
**Flujo**:
```
Widgets → Commands → Software Rasterizer → Framebuffer → SDL_Texture → Display
```
### Por qué Software Rendering
1. **Funciona SIEMPRE** - No depende de drivers GPU
2. **SSH compatible** - X11 forwarding funciona
3. **Máxima compatibilidad** - Desde HP viejo hasta Lenovo nuevo
4. **Simple de debugear** - Es solo un array de pixels
### Fonts
**Dos opciones soportadas**:
1. **Bitmap fonts** (embebidos) - Siempre funcionan, rápidos
2. **TTF** (stb_truetype) - Más flexible, opcional
---
## PLAN DE DESARROLLO
### Fase 0: Setup ✅ COMPLETADA
- [x] Crear estructura de directorios
- [x] build.zig con SDL2
- [x] CLAUDE.md
- [x] Documentación de investigación
### Fase 1: Core + Macros (1 semana)
- [ ] Context con event loop
- [ ] Sistema de macros (grabación/reproducción teclas)
- [ ] Software rasterizer básico (rects, text)
- [ ] SDL2 backend
- [ ] Button, Label (para probar)
### Fase 2: Widgets Esenciales (2 semanas)
- [ ] Input (text entry)
- [ ] Select (dropdown)
- [ ] Checkbox
- [ ] List
- [ ] Layout system
- [ ] Focus management
### Fase 3: Widgets Avanzados (2 semanas)
- [ ] Table con edición
- [ ] Split panels
- [ ] Modal/Popup
- [ ] Panel con título
### Fase 4: Pulido (1 semana)
- [ ] Themes
- [ ] Font handling robusto
- [ ] Documentación
- [ ] Examples completos
---
## REFERENCIAS Y RECURSOS
### Librerías estudiadas
| Librería | Lenguaje | LOC | Valor para nosotros |
|----------|----------|-----|---------------------|
| **microui** | C | 1,100 | Referencia arquitectura mínima |
| **DVUI** | Zig | 15,000 | Único ejemplo Zig native |
| **Dear ImGui** | C++ | 60,000 | API design, features |
| **Gio** | Go | - | Immediate mode moderno |
| **Nuklear** | C | 30,000 | Vertex buffer approach |
### Documentación detallada
- `docs/research/GIO_UI_ANALYSIS.md` - Análisis completo de Gio
- `docs/research/IMMEDIATE_MODE_LIBS.md` - Comparativa de librerías
- `docs/research/SIMIFACTU_FYNE_ANALYSIS.md` - Requisitos de Simifactu
- `docs/ARCHITECTURE.md` - Decisiones de arquitectura
### Links externos
- [microui GitHub](https://github.com/rxi/microui)
- [DVUI GitHub](https://github.com/david-vanderson/dvui)
- [Gio UI](https://gioui.org/)
- [Dear ImGui](https://github.com/ocornut/imgui)
---
## DECISIONES DE DISEÑO CONSENSUADAS
### 1. Immediate Mode vs Retained Mode
**Decisión**: Immediate Mode
**Razón**: Control total, sin threading hell (fyne.Do()), estado explícito
### 2. Software Rendering vs GPU
**Decisión**: Software por defecto, GPU opcional futuro
**Razón**: Máxima compatibilidad (SSH, HP viejo, cualquier driver)
### 3. Sistema de Macros
**Decisión**: Teclas raw, no comandos abstractos
**Razón**: Simple, como Vim, menos código = menos bugs
### 4. Backend inicial
**Decisión**: SDL2
**Razón**: Cross-platform probado, fácil de usar
### 5. Fonts
**Decisión**: Bitmap embebido + TTF opcional
**Razón**: Bitmap siempre funciona, TTF para flexibilidad
### 6. Enfoque de desarrollo
**Decisión**: Híbrido (estudiar DVUI/microui, implementar desde cero)
**Razón**: Aprender haciendo, control total, sin dependencias no deseadas
---
## RELACIÓN CON ZCATUI
**zcatui** (TUI) y **zCatGui** (GUI) son proyectos hermanos:
| Aspecto | zcatui | zCatGui |
|---------|--------|---------|
| Target | Terminal (ANSI) | Ventana gráfica |
| Rendering | Escape codes | Software rasterizer |
| Paradigma | Immediate mode | Immediate mode |
| Layout | Constraint-based | Constraint-based (reusar) |
| Style | Color/Modifier | Color/Style (reusar) |
| Widgets | 35 widgets | En desarrollo |
**Código a reutilizar de zcatui**:
- Sistema de Layout (Constraint, Flex)
- Sistema de Style (Color, Style)
- Conceptos de Focus
- Patterns de widgets
---
## EQUIPO
- **Usuario (Arno)**: Desarrollador principal
- **Claude**: Asistente de programación (Claude Code / Opus 4.5)
---
## NOTAS ZIG 0.15.2
```zig
// Sleep
std.Thread.sleep(ns) // NO std.time.sleep
// ArrayList
var list = std.ArrayList(T).init(allocator);
defer list.deinit();
// HashMap
var map = std.AutoHashMap(K, V).init(allocator);
defer map.deinit();
// Error handling
fn foo() !T { ... }
const result = try foo();
const result = foo() catch |err| { ... };
```
---
## HISTORIAL
| Fecha | Versión | Cambios |
|-------|---------|---------|
| 2025-12-09 | v0.1.0 | Proyecto creado, estructura base, documentación |
---
## ESTADO ACTUAL
**El proyecto está EN FASE INICIAL**
- ✅ Estructura de directorios creada
- ✅ build.zig configurado con SDL2
- ✅ Documentación de investigación completa
- ✅ CLAUDE.md con toda la información
- ⏳ Código fuente pendiente de implementar
**Próximo paso**: Implementar Fase 1 (Core + Macros)

82
build.zig Normal file
View file

@ -0,0 +1,82 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// ===========================================
// Main library module
// ===========================================
const zcatgui_mod = b.createModule(.{
.root_source_file = b.path("src/zcatgui.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
// Link SDL2 to the module
zcatgui_mod.linkSystemLibrary("SDL2", .{});
// ===========================================
// Tests
// ===========================================
const lib_unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/zcatgui.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
lib_unit_tests.root_module.linkSystemLibrary("SDL2", .{});
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
// ===========================================
// Examples
// ===========================================
// Hello World example
const hello_exe = b.addExecutable(.{
.name = "hello",
.root_module = b.createModule(.{
.root_source_file = b.path("examples/hello.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{
.{ .name = "zcatgui", .module = zcatgui_mod },
},
}),
});
hello_exe.root_module.linkSystemLibrary("SDL2", .{});
b.installArtifact(hello_exe);
const run_hello = b.addRunArtifact(hello_exe);
run_hello.step.dependOn(b.getInstallStep());
const hello_step = b.step("hello", "Run hello world example");
hello_step.dependOn(&run_hello.step);
// Macro demo
const macro_exe = b.addExecutable(.{
.name = "macro-demo",
.root_module = b.createModule(.{
.root_source_file = b.path("examples/macro_demo.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{
.{ .name = "zcatgui", .module = zcatgui_mod },
},
}),
});
macro_exe.root_module.linkSystemLibrary("SDL2", .{});
b.installArtifact(macro_exe);
const run_macro = b.addRunArtifact(macro_exe);
run_macro.step.dependOn(b.getInstallStep());
const macro_step = b.step("macro-demo", "Run macro recording demo");
macro_step.dependOn(&run_macro.step);
}

18
build.zig.zon Normal file
View file

@ -0,0 +1,18 @@
.{
.fingerprint = 0x30a5cd33d0b0066c,
.name = .zcatgui,
.version = "0.1.0",
.minimum_zig_version = "0.15.0",
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"examples",
"docs",
"README.md",
"CLAUDE.md",
},
}

529
docs/ARCHITECTURE.md Normal file
View file

@ -0,0 +1,529 @@
# 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:
1. **Máxima compatibilidad** - Funciona en cualquier ordenador (viejo HP, nuevo Lenovo, servidor SSH)
2. **Software rendering** - No depende de GPU/drivers
3. **Sistema de macros** - Piedra angular: grabar y reproducir acciones
4. **Cross-platform** - Linux, Windows, macOS
5. **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
1. **Sin fyne.Do()**: No hay threading hell
2. **Estado explícito**: Debug simple
3. **Testing trivial**: Funciones puras
4. **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 RGBA
- `SoftwareRasterizer`: drawRect, drawLine, drawText
- `Font`: Bitmap fonts + TTF opcional
**Approach: Command List (estilo microui)**
```zig
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 list
- `Layout`: 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):**
1. Table (editable)
2. Input (text entry)
3. Select (dropdown)
4. Panel (con título)
5. Split (HSplit/VSplit)
6. Button
7. 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:
1. KISS - menos código = menos bugs
2. Vim lo hace así y funciona
3. 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:**
1. **Fase 1**: Solo teclado (macros de teclas)
2. **Fase 2**: Mouse → traducir a teclas equivalentes
### 5.4 API
```zig
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
1. **Testing automatizado**: Grabar sesión → test
2. **Tutoriales**: Macros que se ejecutan paso a paso
3. **Repetición**: Tarea repetitiva → hotkey
4. **Debugging**: "¿Cómo llegaste a este bug?" → envía macro
5. **Demos**: Grabar demos que se auto-reproducen
---
## 6. Flujo de Datos
### 6.1 Event Loop Principal
```zig
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
```zig
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, fill
- `Layout`: vertical, horizontal
- `Rect`: x, y, width, height, contains, intersection
### 8.2 Style System
Reutilizar de `zcatui/src/style.zig`:
- `Color`: rgb, indexed, ANSI colors
- `Style`: 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.md`
- `docs/research/IMMEDIATE_MODE_LIBS.md`
- `docs/research/SIMIFACTU_FYNE_ANALYSIS.md`
### Links Externos
- [microui](https://github.com/rxi/microui)
- [DVUI](https://github.com/david-vanderson/dvui)
- [Dear ImGui](https://github.com/ocornut/imgui)
- [Gio](https://gioui.org/)

View file

@ -0,0 +1,431 @@
# Análisis Técnico: Gio UI (Go)
> Investigación realizada: 2025-12-09
> Propósito: Entender arquitectura de Gio como referencia para zCatGui
---
## Resumen Ejecutivo
**Gio UI** (gioui.org) es una librería de GUI immediate mode para Go que permite crear interfaces gráficas cross-platform nativas. Es comparable a Flutter en diseño pero implementado completamente en Go, sin dependencias externas más allá de las librerías de sistema para ventanas y GPU.
| Campo | Valor |
|-------|-------|
| **Repositorio** | https://git.sr.ht/~eliasnaur/gio |
| **Versión** | v0.9.0 (pre-1.0, API inestable) |
| **Licencia** | MIT / Unlicense |
| **Plataformas** | Android, iOS, macOS, Linux, Windows, FreeBSD, OpenBSD, WebAssembly |
---
## 1. Arquitectura General
### 1.1 Estructura de Capas
```
┌─────────────────────────────────────────┐
│ Material Design (widget/material) │ <- Widgets estilizados
├─────────────────────────────────────────┤
│ Widget State (widget) │ <- Estado de controles
├─────────────────────────────────────────┤
│ Layout System (layout) │ <- Sistema de layouts
├─────────────────────────────────────────┤
│ Operations (op, op/clip, op/paint) │ <- Comandos de dibujo
├─────────────────────────────────────────┤
│ Window Management (app) │ <- Manejo de ventanas
├─────────────────────────────────────────┤
│ GPU Rendering (gpu) │ <- Backends gráficos
├─────────────────────────────────────────┤
│ Platform Backend │ <- Win/macOS/Linux/etc.
└─────────────────────────────────────────┘
```
### 1.2 Módulos Principales
| Paquete | Propósito |
|---------|-----------|
| `gioui.org/app` | Window management, eventos del OS |
| `gioui.org/op` | Sistema de operaciones (comandos de dibujo) |
| `gioui.org/layout` | Layouts (Flex, Stack, List, Inset) |
| `gioui.org/widget` | Widgets con estado (Clickable, Editor, List, etc.) |
| `gioui.org/widget/material` | Material Design theme |
| `gioui.org/gpu` | Rendering GPU (OpenGL, Direct3D, Vulkan) |
| `gioui.org/io` | Input/output (pointer, keyboard, clipboard) |
| `gioui.org/text` | Text shaping, layout, glyphs |
| `gioui.org/font` | Font handling, OpenType |
| `gioui.org/gesture` | Gestures de alto nivel (Click, Drag, Scroll, Hover) |
| `gioui.org/x/component` | Componentes extendidos Material |
---
## 2. Sistema de Rendering
### 2.1 GPU Backends
Gio soporta múltiples backends GPU según la plataforma:
- **OpenGL ES** - Multiplataforma (Linux, Android, iOS, WebAssembly)
- **Direct3D 11** - Windows
- **Vulkan** - Linux, Android (opcional)
### 2.2 Vector Rendering Pipeline
Gio usa un **vector renderer basado en Pathfinder** (proyecto de Mozilla):
1. **Renderizado de outlines**: Las formas y texto se renderizan desde sus contornos vectoriales
2. **Sin pre-procesamiento CPU**: Los paths SVG se renderizan en GPU con mínimo trabajo en CPU
3. **Migración a piet-gpu**: Renderer basado en compute shaders para mayor eficiencia
### 2.3 CPU Fallback
**Importante para zCatGui**: Gio incluye un fallback CPU (`gioui.org/cpu`) con binarios pre-compilados de piet-gpu para arm, arm64, amd64, permitiendo renderizado software cuando no hay GPU disponible.
---
## 3. Sistema de Layout
### 3.1 Conceptos Fundamentales
```go
// Constraints = lo que el padre dice al hijo (límites de tamaño)
type Constraints struct {
Min, Max image.Point
}
// Dimensions = lo que el hijo devuelve al padre (tamaño usado)
type Dimensions struct {
Size image.Point
Baseline int
}
// Context = bundle de estado pasado a todos los widgets
type Context struct {
Constraints Constraints
Metric unit.Metric
Now time.Time
// ...
}
// Widget = función que calcula dimensions desde constraints
type Widget func(gtx layout.Context) layout.Dimensions
```
### 3.2 Layouts Principales
#### Flex - Distribución lineal con pesos
```go
layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D { return header.Layout(gtx) }),
layout.Flexed(1, func(gtx C) D { return content.Layout(gtx) }),
layout.Rigid(func(gtx C) D { return footer.Layout(gtx) }),
)
```
**Spacing modes:**
- `SpaceEnd` - Espacio al final
- `SpaceStart` - Espacio al inicio
- `SpaceSides` - Espacio en ambos lados
- `SpaceAround` - Espacio alrededor de children
- `SpaceBetween` - Espacio entre children
- `SpaceEvenly` - Distribución uniforme
#### Stack - Elementos apilados
```go
layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx C) D { return image.Layout(gtx) }),
layout.Stacked(func(gtx C) D { return button.Layout(gtx) }),
)
```
#### List - Lista scrollable virtualizada
```go
// Solo renderiza los elementos visibles
list.Layout(gtx, 1_000_000, func(gtx C, i int) D {
return material.Label(theme, unit.Sp(16), fmt.Sprintf("Item %d", i)).Layout(gtx)
})
```
#### Inset - Padding
```go
layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return widget.Layout(gtx)
})
```
### 3.3 Sistema de Unidades
```go
type Px // Physical pixels (dependiente de dispositivo)
type Dp // Device-independent pixels (escala por densidad)
type Sp // Scaled points (incluye scaling de fuente del sistema)
```
---
## 4. Sistema de Eventos
### 4.1 Tipos de Eventos
```go
type pointer.Event // Mouse/touch (Press, Release, Move, Drag, Scroll)
type key.Event // Keyboard
type key.EditEvent // Edición de texto
type key.FocusEvent // Cambio de foco
```
### 4.2 Distribución de Eventos (Tag-based)
Gio usa un sistema **sin callbacks** basado en **tags**:
```go
// Declarar área clickable
pointer.InputOp{
Tag: &button, // Tag único
Types: pointer.Press | pointer.Release,
}.Add(ops)
// Consultar eventos
for {
ev, ok := gtx.Event(pointer.Filter{Target: &button, ...})
if !ok { break }
// Procesar evento
}
```
**Ventaja**: Elimina el overhead de callbacks y simplifica el manejo de estado.
### 4.3 Gestures de Alto Nivel
```go
type Click // Detecta clicks
type Drag // Detecta drags
type Hover // Detecta hover
type Scroll // Detecta scroll
```
---
## 5. Widgets Incluidos
### 5.1 Widget Pattern (Separación de Concerns)
Gio separa **estado** (stateful) y **presentación** (stateless):
```
widget.Clickable (state) → material.Button (presentation)
widget.Editor (state) → material.Editor (presentation)
widget.Bool (state) → material.CheckBox (presentation)
```
### 5.2 Widget State (`gioui.org/widget`)
| Widget | Propósito |
|--------|-----------|
| `Clickable` | Área clickable (botones, cards, etc.) |
| `Editor` | Editor de texto de una línea |
| `Selectable` | Texto seleccionable (no editable) |
| `Float` | Valor float para sliders |
| `Bool` | Valor booleano (checkboxes, switches) |
| `Enum` | Valor enum (radio buttons) |
| `List` | Estado de lista scrollable |
| `Scrollbar` | Estado de scrollbar |
| `Draggable` | Soporte drag & drop |
| `Decorations` | Decoraciones de ventana |
| `Icon` | Ícono vectorial |
### 5.3 Material Widgets (`gioui.org/widget/material`)
**Texto:** Label, H1-H6, Body1, Body2, Caption, Subtitle1, Subtitle2, Overline
**Botones:** Button, IconButton, ButtonLayout
**Input:** Editor, CheckBox, RadioButton, Switch, Slider
**Contenedores:** List, Scrollbar
**Indicadores:** ProgressBar, ProgressCircle, Loader (spinner)
**Ventanas:** Decorations
### 5.4 Extended Components (`gioui.org/x/component`)
- AppBar, NavDrawer, Menu, MenuItem, ContextArea
- Grid, Table, Sheet, Surface
- TextField (con label animada), Tooltip
- Discloser (expandible), Divider
- ModalLayer, Scrim
---
## 6. Theming y Styling
### 6.1 Sistema de Theme
```go
type Theme struct {
Shaper *text.Shaper
Palette Palette
TextSize unit.Sp
Face text.Face
FingerSize unit.Dp // Tamaño mínimo área touch (48dp)
}
type Palette struct {
Bg color.NRGBA
Fg color.NRGBA
ContrastBg color.NRGBA
ContrastFg color.NRGBA
}
```
### 6.2 Niveles de Customización
**Global (Theme-wide):**
```go
theme := material.NewTheme()
theme.Palette.Fg = color.NRGBA{R: 255, G: 255, B: 255, A: 255}
```
**Widget-local:**
```go
btn := material.Button(theme, button, "Click me!")
btn.Background = color.NRGBA{R: 0, G: 150, B: 0, A: 255}
```
---
## 7. Window Management
### 7.1 Creación de Ventana
```go
func main() {
go func() {
window := new(app.Window)
window.Option(app.Title("My App"))
window.Option(app.Size(unit.Dp(800), unit.Dp(600)))
if err := run(window); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main() // BLOQUEANTE en algunas plataformas
}
```
### 7.2 Event Loop
```go
func run(window *app.Window) error {
var ops op.Ops
for {
switch e := window.Event().(type) {
case app.DestroyEvent:
return e.Err
case app.FrameEvent:
ops.Reset()
gtx := app.NewContext(&ops, e)
drawUI(gtx)
e.Frame(gtx.Ops)
}
}
}
```
---
## 8. Paradigma Immediate Mode
### 8.1 Filosofía
```
while running:
eventos ← poll_events()
actualizar_estado(eventos)
ops.Reset()
dibujar_ui_completa(ops, estado) ← UI es función pura del estado
enviar_a_gpu(ops)
```
### 8.2 Ventajas Específicas de Gio
**Sin callbacks:**
```go
// NO HAY ESTO:
button.onClick(func() { ... })
// EN SU LUGAR:
for button.Clicked(gtx) {
count++
}
```
**Estado mínimo:**
```go
// El estado es tuyo, no del framework
type State struct {
count int
name string
}
// Widgets solo contienen estado UI mínimo
var button widget.Clickable
```
**UI como función pura:**
```go
func drawUI(gtx layout.Context, state *State) {
label := material.H1(theme, fmt.Sprintf("Count: %d", state.count))
label.Layout(gtx)
}
```
### 8.3 Zero-Allocation Design
```go
// Método tipado, sin interfaz → sin allocación
operation.Add(ops)
```
---
## 9. Lecciones para zCatGui
### 9.1 Qué Adoptar
1. **Separación estado/presentación**: Widget state vs widget render
2. **Sistema de constraints**: Constraints/Dimensions muy robusto
3. **Event tags**: Sin callbacks, consulta de eventos
4. **Zero-allocation**: Operaciones tipadas
5. **Spacing modes**: Flex con SpaceBetween, SpaceAround, etc.
### 9.2 Qué Adaptar
1. **Rendering**: Gio usa GPU, nosotros software renderer
2. **Operaciones**: Gio usa op.Ops (GPU), nosotros DrawCommand (CPU)
3. **Complejidad**: Gio es ~40K LOC, nosotros apuntamos a ~5-10K
### 9.3 Diferencias Clave
| Aspecto | Gio | zCatGui |
|---------|-----|---------|
| Lenguaje | Go | Zig |
| Rendering | GPU (Pathfinder) | Software (framebuffer) |
| Complejidad | Alta (~40K LOC) | Baja (target ~5K) |
| Macros | No tiene | Piedra angular |
| Fonts | HarfBuzz | Bitmap + stb_truetype |
---
## 10. Referencias
- [Gio UI Website](https://gioui.org/)
- [Architecture](https://gioui.org/doc/architecture)
- [Layout](https://gioui.org/doc/architecture/layout)
- [Widget](https://gioui.org/doc/architecture/widget)
- [Input](https://gioui.org/doc/architecture/input)
- [API Reference](https://pkg.go.dev/gioui.org)
- [Examples](https://github.com/gioui/gio-example)
- [Extended Components](https://github.com/gioui/gio-x)
- [GUI with Gio Tutorial](https://jonegil.github.io/gui-with-gio/)
- [Immediate Mode GUI Programming](https://eliasnaur.com/blog/immediate-mode-gui-programming)

View file

@ -0,0 +1,502 @@
# Análisis Comparativo: Librerías GUI Immediate-Mode
> Investigación realizada: 2025-12-09
> Propósito: Identificar mejores referencias para implementar zCatGui
---
## Resumen Ejecutivo
Se analizaron las principales librerías GUI immediate-mode para identificar patrones, arquitecturas y código reutilizable para zCatGui.
### Ranking de Relevancia para zCatGui
| # | Librería | Relevancia | Por qué |
|---|----------|------------|---------|
| 1 | **microui** | ⭐⭐⭐⭐⭐ | 1,100 LOC, C puro, software rendering natural |
| 2 | **DVUI** | ⭐⭐⭐⭐⭐ | Único ejemplo Zig nativo |
| 3 | **Dear ImGui** | ⭐⭐⭐⭐ | Referencia API, muy maduro |
| 4 | **Nuklear** | ⭐⭐⭐⭐ | C puro, vertex buffer approach |
| 5 | **egui** | ⭐⭐⭐ | API ergonómica (Rust) |
| 6 | **raygui** | ⭐⭐⭐ | Ya tiene bindings Zig |
---
## 1. microui (C) ⭐⭐⭐⭐⭐
### Información General
| Campo | Valor |
|-------|-------|
| **Repositorio** | https://github.com/rxi/microui |
| **Lenguaje** | ANSI C |
| **LOC** | ~1,100 (microui.h + microui.c) |
| **Licencia** | MIT |
### Por qué es la Mejor Referencia
1. **Tamaño perfecto**: 1,100 LOC se puede estudiar completo en una tarde
2. **C puro**: Trivial de entender para quien escribe Zig
3. **Software rendering natural**: Output son comandos primitivos (rect, text)
4. **Zero dependencies**: ANSI C puro
5. **Diseño brillante**: Máximo valor en mínimo espacio
### Arquitectura
```
Input → mu_Context → Widgets → Command List → Render
```
**Command List Approach:**
```c
// Output son comandos simples
enum { MU_COMMAND_RECT, MU_COMMAND_TEXT, MU_COMMAND_CLIP, ... };
// Fácil de renderizar con software
while (mu_next_command(ctx, &cmd)) {
switch (cmd->type) {
case MU_COMMAND_RECT: draw_rect(...);
case MU_COMMAND_TEXT: draw_text(...);
}
}
```
### Widgets Incluidos (8)
- Window, Panel
- Button, Checkbox, Slider
- Textbox, Label
- (containers básicos)
### Código Ejemplo
```c
mu_begin_window(ctx, "My Window", mu_rect(10, 10, 300, 200));
mu_layout_row(ctx, 2, (int[]){100, -1}, 0);
mu_label(ctx, "Name:");
if (mu_textbox(ctx, name_buf, sizeof(name_buf)) & MU_RES_SUBMIT) {
submit_name();
}
if (mu_button(ctx, "Submit")) {
handle_submit();
}
mu_end_window(ctx);
```
### Lecciones para zCatGui
1. **Command list es suficiente** - No necesitamos vertex buffers
2. **~1000 LOC para MVP** - Es alcanzable
3. **State pool** - Técnica para tracking de IDs
4. **Layout simple** - `mu_layout_row()` es todo lo que necesitas inicialmente
### Recursos
- [microui GitHub](https://github.com/rxi/microui)
- [microui v2 Implementation Overview](https://rxi.github.io/microui_v2_an_implementation_overview.html)
---
## 2. DVUI (Zig) ⭐⭐⭐⭐⭐
### Información General
| Campo | Valor |
|-------|-------|
| **Repositorio** | https://github.com/david-vanderson/dvui |
| **Lenguaje** | Zig |
| **LOC** | ~15,000 (estimado) |
| **Estado Zig** | 0.13-0.15 |
### Por qué es Crítico
**Es la ÚNICA GUI immediate-mode nativa en Zig.**
1. **Idiomático Zig**: Patterns que podemos copiar directamente
2. **Moderno**: Low framerate support (no redibuja innecesariamente)
3. **Backend abstraction**: VTable pattern para SDL3, Raylib, Web, DirectX11
4. **Activo**: Mantenido activamente (2022-presente)
### Arquitectura
```
Event → Widget Processing → Layout → Render
```
**Características especiales:**
- Procesa TODOS los eventos (no solo último)
- Funciona a bajo framerate
- Floating windows con Z-order correcto
### Widgets Incluidos (~30)
- Button, Checkbox, Radio
- TextInput, TextArea
- Slider, ScrollArea
- Menu, Dropdown
- TreeView, Table
- Modal, Popup
- y más...
### Código Ejemplo (Zig)
```zig
pub fn gui(dvui: *Dvui) !void {
if (dvui.button("Click me!")) {
count += 1;
}
dvui.text("Count: {}", .{count});
if (dvui.beginWindow("Settings")) {
defer dvui.endWindow();
_ = dvui.checkbox("Enable feature", &enabled);
_ = dvui.slider("Volume", &volume, 0, 100);
}
}
```
### Lecciones para zCatGui
1. **Backend VTable**: Abstracción limpia para múltiples backends
2. **Event processing**: Cómo manejar eventos en Zig
3. **Widget patterns**: Cómo estructurar widgets en Zig
4. **Memory management**: Allocators idiomáticos
### Recursos
- [DVUI GitHub](https://github.com/david-vanderson/dvui)
- [DVUI Zig NEWS Article](https://zig.news/david_vanderson/dvui-immediate-zig-gui-for-apps-and-games-2a5h)
---
## 3. Dear ImGui (C++) ⭐⭐⭐⭐
### Información General
| Campo | Valor |
|-------|-------|
| **Repositorio** | https://github.com/ocornut/imgui |
| **Lenguaje** | C++ |
| **LOC** | ~60,000 |
| **Licencia** | MIT |
### Por qué es Importante
**El estándar de facto para GUIs immediate-mode.**
1. **Battle-tested**: Usado en millones de aplicaciones
2. **API excelente**: Muy ergonómica y bien diseñada
3. **Documentación**: FAQ, wiki, ejemplos abundantes
4. **Software renderer existe**: [imgui_software_renderer](https://github.com/emilk/imgui_software_renderer)
### Arquitectura
```
Input → ImGuiContext → Widgets → Vertex Buffers → GPU/Software Raster
```
**Separación de concerns:**
- **Core**: imgui.cpp, imgui_widgets.cpp, imgui_tables.cpp, imgui_draw.cpp
- **Backends**: Plataforma (GLFW, SDL2/3, Win32) separados
- **Renderers**: OpenGL, DirectX, Vulkan, Metal, WebGPU
### Widgets Incluidos (50+)
- Window, Child, Popup, Modal
- Button, Checkbox, Radio, Slider, Drag
- Input (text, int, float, multiline)
- Combo, ListBox, Selectable
- TreeNode, CollapsingHeader
- Table (muy avanzada)
- Menu, MenuBar
- TabBar, Tabs
- ColorPicker, ColorEdit
- Plot, Histogram
- y muchos más...
### Lecciones para zCatGui
1. **ID system**: Hash de string/pointer para tracking
2. **State caching**: ImGuiStorage para estado persistente
3. **Table API**: Referencia para nuestra tabla
4. **Backend separation**: Muy limpia
### Bindings Zig Existentes
- [cimgui.zig](https://github.com/tiawl/cimgui.zig)
- [dinau/imguinz](https://github.com/dinau/imguinz) - ImGui 1.91.8
- [SpexGuy/Zig-ImGui](https://github.com/SpexGuy/Zig-ImGui)
### Recursos
- [Dear ImGui GitHub](https://github.com/ocornut/imgui)
- [Dear ImGui FAQ](https://github.com/ocornut/imgui/blob/master/docs/FAQ.md)
- [imgui_software_renderer](https://github.com/emilk/imgui_software_renderer)
---
## 4. Nuklear (C) ⭐⭐⭐⭐
### Información General
| Campo | Valor |
|-------|-------|
| **Repositorio** | https://github.com/Immediate-Mode-UI/Nuklear |
| **Lenguaje** | ANSI C |
| **LOC** | ~30,000 (single header) |
| **Licencia** | Public Domain |
### Características
1. **Single header**: nuklear.h contiene todo
2. **Renderer-agnostic**: Command iteration o vertex buffers
3. **Zero dependencies**: ANSI C puro
4. **Memoria fija**: Diseñado para embedded
### Arquitectura
**Dos modos de rendering:**
1. **Command iteration** (similar a microui):
```c
nk_foreach(cmd, &ctx->draw_list) {
switch (cmd->type) {
case NK_COMMAND_RECT: ...
case NK_COMMAND_TEXT: ...
}
}
```
2. **Vertex buffer** (similar a ImGui):
```c
nk_convert(&ctx, &cmds, &verts, &idx, &config);
// Renderizar con GPU
```
### Widgets Incluidos (~20)
- Window, Panel, Group
- Button, Checkbox, Radio, Slider
- Edit (text), Property
- Chart, Color picker
- Combo, Contextual, Menu, Tree
### Lecciones para zCatGui
1. **Styling extenso**: Sistema de propiedades muy configurable
2. **Memory model**: Fixed memory para embedded
3. **Dual rendering**: Command list Y vertex buffer
### Recursos
- [Nuklear GitHub](https://github.com/Immediate-Mode-UI/Nuklear)
---
## 5. egui (Rust) ⭐⭐⭐
### Información General
| Campo | Valor |
|-------|-------|
| **Repositorio** | https://github.com/emilk/egui |
| **Lenguaje** | Rust |
| **LOC** | ~35,000 |
| **Licencia** | MIT / Apache 2.0 |
### Características
1. **API ergonómica**: Closures, builder pattern idiomático
2. **Web + Native**: Diseñado para ambos desde el inicio
3. **epaint**: Tessellator separado (shapes → meshes)
### Arquitectura
```
egui (core) → epaint (tessellation) → eframe (backend glue)
```
**epaint es interesante**: Convierte shapes vectoriales en triangle meshes.
### Lecciones para zCatGui
1. **Response pattern**: Widgets retornan Response con interacciones
2. **Context memory**: Retiene mínimo estado entre frames
3. **Tessellator separado**: epaint podría portarse
### Nota sobre Software Rendering
**No tiene software renderer oficial** - hay un issue abierto (#1129).
El autor (emilk) escribió uno para ImGui, podría adaptarse.
### Recursos
- [egui GitHub](https://github.com/emilk/egui)
- [egui.rs](https://www.egui.rs/)
---
## 6. raygui (C) ⭐⭐⭐
### Información General
| Campo | Valor |
|-------|-------|
| **Repositorio** | https://github.com/raysan5/raygui |
| **Lenguaje** | C |
| **LOC** | ~8,000 |
| **Dependencia** | raylib |
### Características
1. **Integrado con raylib**: Dibuja directamente
2. **Tools-focused**: Diseñado para editores
3. **Ya tiene bindings Zig**: via raylib-zig
### Por qué Menos Relevante
- Acoplado a raylib
- Menos features que ImGui/Nuklear
- No standalone
### Recursos
- [raygui GitHub](https://github.com/raysan5/raygui)
- [raylib-zig](https://github.com/raylib-zig/raylib-zig)
---
## Comparativa Técnica
| Librería | LOC | Lenguaje | Software Renderer | Complejidad | Porting a Zig |
|----------|-----|----------|-------------------|-------------|---------------|
| **microui** | 1,100 | C | ✅ Natural | Muy Baja | Trivial |
| **DVUI** | 15,000 | Zig | Via backend | Media | ✅ Nativo |
| **Dear ImGui** | 60,000 | C++ | ✅ (external) | Alta | Medio |
| **Nuklear** | 30,000 | C | ⚠️ Custom | Media | Fácil |
| **egui** | 35,000 | Rust | ❌ Planeado | Media | Medio |
| **raygui** | 8,000 | C | Via raylib | Baja | Ya existe |
---
## Arquitectura de Rendering: Comparativa
### Command List Approach (microui, nuestro approach)
```
Input → UI Logic → Commands (DrawRect, DrawText) → Direct Render
```
**Pros**: Simplicísimo, ideal software rendering
**Contras**: No batching, menos eficiente con muchos elementos
### Vertex Buffer Approach (ImGui, Nuklear, egui)
```
Input → UI Logic → Shapes → Tessellate → Vertex Buffers → GPU/Software Raster
```
**Pros**: Eficiente, batching, flexible
**Contras**: Necesita tessellator, más complejo
### Decisión para zCatGui
**Command List** (microui style):
1. Más simple de implementar
2. Suficiente para nuestros casos de uso
3. Software rendering natural
4. ~1000 LOC para renderer
---
## Software Rendering: Análisis
### ¿Qué necesita un software renderer?
1. **Framebuffer**: Array 2D de pixels (u32 RGBA)
2. **Primitive rasterizer**:
- `drawRect(x, y, w, h, color)`
- `drawLine(x1, y1, x2, y2, color)`
- Opcional: `drawTriangle()` para anti-aliasing
3. **Text rasterizer**:
- Font atlas (bitmap)
- `drawText(x, y, string, font, color)`
4. **Clipping**: Restringir dibujo a región
5. **Output**: Blit framebuffer a ventana (SDL_UpdateTexture)
### Librerías con Mejor Soporte Software
1. **microui**: IDEAL - output son rects/text directos
2. **imgui_software_renderer**: Rasteriza triangles de ImGui en CPU
3. **SDL software renderer**: Backend para ImGui/Nuklear
### Estimación para zCatGui
- **Rasterizer básico**: ~500 LOC
- **Font handling**: ~300 LOC
- **Clipping**: ~100 LOC
- **Total**: ~1000 LOC
---
## Recomendaciones para zCatGui
### Estudio Prioritario
1. **microui** (1-2 días): Código completo, fundamentos
2. **DVUI** (2-3 días): Patterns Zig, backend abstraction
3. **Dear ImGui** (1 semana): API design (no portar, solo estudiar)
### Arquitectura Propuesta
```zig
// Basado en microui + DVUI patterns
pub const Context = struct {
commands: ArrayList(DrawCommand),
state: StatePool,
input: InputState,
pub fn button(self: *Context, text: []const u8) bool {
const id = hashId(text);
const bounds = self.layout.nextRect();
const hovered = bounds.contains(self.input.mouse);
const clicked = hovered and self.input.mousePressed;
self.pushCommand(.{ .rect = .{ .bounds = bounds, ... }});
self.pushCommand(.{ .text = .{ .pos = bounds.center(), ... }});
return clicked;
}
};
```
### Roadmap Basado en Referencias
| Fase | LOC Estimado | Tiempo | Referencia Principal |
|------|--------------|--------|---------------------|
| MVP (button, label) | ~1000 | 1 semana | microui |
| Core widgets | ~3000 | 2 semanas | microui + DVUI |
| Table editable | ~1500 | 2 semanas | Dear ImGui |
| Total | ~5500 | 5 semanas | - |
---
## Recursos Clave
### Tutoriales y Teoría
- [Immediate Mode GUI Wikipedia](https://en.wikipedia.org/wiki/Immediate_mode_GUI)
- [Retained vs Immediate Mode - Microsoft](https://learn.microsoft.com/en-us/windows/win32/learnwin32/retained-mode-versus-immediate-mode)
- [microui v2 Implementation Overview](https://rxi.github.io/microui_v2_an_implementation_overview.html)
### Software Rendering
- [imgui_software_renderer](https://github.com/emilk/imgui_software_renderer)
- [Tiny CPU Rasterizer Tutorial](https://lisyarus.github.io/blog/posts/implementing-a-tiny-cpu-rasterizer-part-1.html)
### Zig Bindings Existentes
- [cimgui.zig](https://github.com/tiawl/cimgui.zig)
- [raylib-zig](https://github.com/raylib-zig/raylib-zig)

View file

@ -0,0 +1,617 @@
# Análisis: Widgets y Funcionalidades de Fyne en Simifactu
> Investigación realizada: 2025-12-09
> Propósito: Extraer requisitos reales de una aplicación de producción
---
## Resumen Ejecutivo
**Simifactu** es una aplicación de facturación empresarial desarrollada en Go con Fyne v2. Este análisis extrae todos los widgets, layouts, y funcionalidades que zCatGui necesitaría para soportar una aplicación similar.
**Proyecto analizado**: `/mnt/cello2/arno/re/recode/go/simifactu/`
---
## 1. Widgets Básicos Usados
### 1.1 Entry Widgets (Campos de Texto)
**Uso masivo**: Prácticamente todos los paneles usan Entry fields.
**Variantes detectadas:**
- `widget.Entry` - Campo de texto básico (single-line)
- `widget.Entry` con `MultiLine = true` - Texto multi-línea
- **AutoComplete Entry** (custom) - Entry con dropdown de sugerencias
**Características críticas:**
- `OnChanged` callback ejecutado en cada tecla
- `OnSubmit` callback al presionar Enter
- `PlaceHolder` text
- `Validation` callbacks
- **ReadOnly** mode
- `FocusGained/FocusLost` callbacks
**Requisito zCatGui:**
```zig
pub const Input = struct {
text: []const u8,
cursor: usize,
placeholder: []const u8,
multiline: bool,
suggestions: ?[][]const u8, // Para autocomplete
on_changed: ?*const fn([]const u8) void,
on_submit: ?*const fn([]const u8) void,
readonly: bool,
};
```
### 1.2 Button Widgets
**Variantes detectadas:**
- `widget.Button` - Botón estándar
- **Button3D** (custom) - Botón con efecto 3D, altura reducida
**Usos identificados:**
- Botones CRUD: Nuevo, Guardar, Eliminar
- Navegación: `<<`, `<`, `>`, `>>`
- Acciones contextuales: Exportar, Importar, Duplicar
**Requisito zCatGui:**
```zig
pub const Button = struct {
label: []const u8,
on_tapped: *const fn() void,
disabled: bool,
style: Style,
importance: enum { primary, secondary, danger },
};
```
### 1.3 Select/Dropdown Widgets
**Variantes detectadas:**
- `widget.Select` - Dropdown básico
- **SelectEntry** - Combo editable (dropdown + entry)
**Usos identificados:**
- Tipo IVA: "21%", "10%", "4%", "0%", "Exento"
- Régimen Equivalencia
- Tipo Documento: "Presupuesto", "Albarán", "Factura"
- Estado Documento: "Borrador", "Confirmado", "Enviado"
- Forma de Pago
**Requisito zCatGui:**
```zig
pub const Select = struct {
options: [][]const u8,
selected: usize,
on_changed: *const fn(usize) void,
placeholder: []const u8,
allow_custom: bool,
};
```
### 1.4 Checkbox
**Usos detectados:**
- "Es Sociedad"
- Selección múltiple en importación
**Requisito zCatGui:**
```zig
pub const Checkbox = struct {
checked: bool,
label: []const u8,
on_changed: *const fn(bool) void,
};
```
---
## 2. Widgets Avanzados/Complejos
### 2.1 Table Widget ⭐ CRÍTICO
**El widget MÁS importante** de Simifactu - usado en prácticamente todas las vistas.
**Implementación actual:**
- Fork personalizado de `fyne/widget/table.go`
- **AdvancedTable** wrapper (1000+ líneas)
**Características implementadas (TODAS necesarias):**
#### Virtualización y Performance
- Lazy rendering (solo filas visibles)
- Scroll eficiente con cache de celdas
- Invalidación selectiva de cache
#### Navegación Teclado
```
Flechas: Up/Down/Left/Right (navegación entre celdas)
Enter: Editar celda actual
Space: Activar edición con celda vacía
Tab: Siguiente celda editable
Escape: Cancelar edición / Revertir cambios
Ctrl+N: Nueva fila
Ctrl+B/Supr: Borrar fila
Home/End: Primera/Última fila
```
#### Edición In-Situ
- Doble-click para editar celda
- Entry overlay que aparece sobre celda
- Auto-submit al cambiar fila
- Validación por columna
- **Tipos de celda**: Text, Number, Money, Date, Select
#### Indicadores Visuales de Estado
- Filas nuevas: Icono verde
- Filas modificadas: Icono naranja
- Filas borradas: Icono rojo
#### Ordenamiento (Sorting)
- Click en header para ordenar
- Indicadores visuales ▲/▼
#### Colores Dinámicos
```zig
pub const TableColors = struct {
header_background: Color,
header_text: Color,
row_normal: Color,
row_hover: Color,
cell_active: Color,
cell_editing: Color,
selection: Color,
};
```
#### Schema-Driven Configuration
```zig
pub const TableSchema = struct {
columns: []ColumnDef,
show_state_indicators: bool,
allow_keyboard_nav: bool,
allow_edit: bool,
allow_sort: bool,
};
pub const ColumnDef = struct {
name: []const u8,
width: f32,
column_type: enum { text, number, money, date, select },
editable: bool,
validator: ?*const fn([]const u8) bool,
};
```
### 2.2 InnerWindow Widget ⭐ CRÍTICO
**Fork personalizado de Fyne** - contenedor con barra de título y bordes.
**Características:**
- Título personalizable y dinámico
- Barra de título con color configurable
- Borde exterior con color configurable
- Fondo contenido separado
- Botón cerrar opcional
- Callbacks: `OnFocusGained`, `OnFocusLost`, `OnTappedBar`
- Resize programático
**Uso en Simifactu:**
- Todos los paneles principales están en InnerWindows
- Layout: 3-4 InnerWindows en HSplit/VSplit
**Requisito zCatGui:**
```zig
pub const Panel = struct {
title: []const u8,
content: Widget,
border: BorderStyle,
title_style: Style,
closable: bool,
on_focus_gained: ?*const fn() void,
on_focus_lost: ?*const fn() void,
};
```
### 2.3 List Widget
**Uso detectado:**
- Navegador estilo OpenOffice (carpetas izquierda, archivos derecha)
**Requisito zCatGui:**
```zig
pub const List = struct {
items: []ListItem,
selected: i32,
on_selected: *const fn(usize) void,
};
pub const ListItem = struct {
text: []const u8,
icon: ?Symbol,
};
```
---
## 3. Layouts Usados
### 3.1 Container Layouts (Fyne)
```go
// Border - contenido con bordes fijos
container.NewBorder(top, bottom, left, right, center)
// VBox/HBox - stacks
container.NewVBox(widget1, widget2, widget3)
container.NewHBox(widget1, widget2)
// Grid
container.NewGridWithColumns(3, campo1, campo2, campo3)
// HSplit/VSplit - CRÍTICO
split := container.NewHSplit(leftPanel, rightPanel)
split.SetOffset(0.25) // 25% izquierda
// Stack - overlay
container.NewStack(background, content, overlayPopup)
// Padded
container.NewPadded(widget)
```
### 3.2 Layout Principal de Simifactu
```
┌───────────────────────────────────────────────────────────┐
│ Status Line (Border Top - altura fija) │
├─────────┬─────────────────────┬────────────────────────────┤
│ WHO │ WHO Detail │ Document Detail (FULL) │
│ List │ ─────────────────── │ - Cabecera (40%) │
│ (20%) │ Documents List │ - Líneas (60%) │
│ │ │ │
│ │ (VSplit 50/50) │ (VSplit integrado) │
│ │ (35%) │ (45%) │
└─────────┴─────────────────────┴────────────────────────────┘
HSplit(0.20) HSplit(0.4347)
```
**Requisito zCatGui:**
```zig
pub const Split = struct {
direction: enum { horizontal, vertical },
children: [2]Widget,
offset: f32, // 0.0-1.0
draggable: bool, // Ctrl+flechas para ajustar
};
```
---
## 4. Diálogos y Popups
### 4.1 Modal Dialogs Detectados
```go
// Confirmación (Yes/No)
dialog.ShowConfirm("Eliminar", "¿Seguro?", callback, window)
// Información (solo OK)
dialog.ShowInformation("Título", "Mensaje", window)
// Error (solo OK)
dialog.ShowError(err, window)
// Entrada de texto
dialog.ShowEntryDialog("Nombre", "Introduzca:", "default", callback, window)
// File picker
dialog.ShowFileOpen(callback, window)
// Folder picker
dialog.ShowFolderOpen(callback, window)
```
### 4.2 Custom Dialogs Detectados
- **Import Window** - Navegador con checkboxes
- **Export Window** - Selector formato + opciones
- **Edit Date Dialog** - Picker fecha con calendario
- **Empresas Window** - Gestión multi-empresa
---
## 5. Eventos y Callbacks
### 5.1 Eventos Teclado ⭐ MUY IMPORTANTE
**OnTypedKey** - Usado en TODOS los paneles:
```go
window.Canvas().SetOnTypedKey(func(key *fyne.KeyEvent) {
// Shortcuts globales
if key.Name == fyne.KeyF2 {
showEmpresasWindow()
return
}
// Ctrl+1/2/3 - Layout presets
if mods&fyne.KeyModifierControl != 0 {
switch key.Name {
case "1": applyLayout1()
case "2": applyLayout2()
}
}
})
```
**Requisito zCatGui:**
```zig
pub const KeyEvent = struct {
key: Key,
modifiers: KeyModifiers,
};
pub const KeyModifiers = packed struct {
control: bool,
alt: bool,
shift: bool,
};
// Handler retorna si consume el evento
pub const OnKey = *const fn(KeyEvent) bool;
```
### 5.2 Eventos Mouse
```go
widget.OnTapped = func(*fyne.PointEvent) { click() }
table.OnCellDoubleTapped = func(row, col int) { edit(row, col) }
widget.OnMouseIn = func(*desktop.MouseEvent) { hover() }
widget.OnMouseOut = func(*desktop.MouseEvent) { unhover() }
```
### 5.3 Focus Events
```go
widget.OnFocusGained = func() { activate() }
widget.OnFocusLost = func() { deactivate() }
canvas.Focus(widget) // Programático
```
---
## 6. El Problema fyne.Do() ⭐⭐⭐ CRÍTICO
### 6.1 El Problema del Threading
**402 usos de `fyne.Do()`** detectados en el proyecto.
**Razón**: Fyne requiere que TODAS las operaciones UI se ejecuten en el thread principal.
```go
// PATTERN OBLIGATORIO en Fyne
go func() {
result := heavyComputation() // Background
fyne.Do(func() {
label.SetText(result) // DEBE estar en fyne.Do()
table.Refresh()
})
}()
```
**Errores si no se usa fyne.Do():**
```
panic: concurrent map writes
SIGSEGV: segmentation fault
GL context not current
```
### 6.2 Por qué zCatGui NO tiene este problema
**Immediate mode es inherentemente thread-safe por diseño:**
- No hay estado compartido del framework
- Buffer es solo memoria (no GL context)
- Render es single-threaded
- No necesita equivalente a fyne.Do()
**Pero sí necesitamos:**
- AsyncLoop para queries BD
- Timer system para auto-save
- Event queue
---
## 7. Theming y Colores
### 7.1 Sistema de Colores Dual
**Sistema 1: Colores por Panel:**
```go
type PanelColors struct {
Fondo Color
Header Color
Texto Color
Entry_Fondo Color
Entry_Texto Color
Button_Fondo Color
Tabla_Header Color
Celda_Activa Color
// ... 20+ colores por panel
}
dataManager.GetColorForPanel("who_detail", "Fondo")
```
**Sistema 2: Colores Globales Botones:**
```go
type ButtonColors struct {
ActivoButtonFondo Color
InactivoButtonFondo Color
NuevoButtonFondo Color
EliminarButtonFondo Color
}
```
### 7.2 Hot-Reload de Colores
```go
// Panel se registra como observer
dataManager.RegisterColorObserver("who_detail", panel)
// Cuando usuario cambia color
dataManager.NotifyColorsChange("who_detail")
// Panel actualiza
func (p *Panel) NotifyColorsChange() {
p.applyColors()
}
```
### 7.3 Persistencia
- Archivo texto KV: `who_detail.Fondo = 40,44,52`
- JSON backup
- Base de datos: tabla `config_backup`
---
## 8. Características Especiales
### 8.1 Auto-Save System
```go
dataManager.StartAutoSaveTimer()
// Guarda config cada N segundos si isDirty == true
```
### 8.2 File Watcher (Hot-Reload Config)
```go
dataManager.StartConfigFileWatcher()
// Detecta cambios externos, recarga automáticamente
```
### 8.3 Export/Import Data
- JSON completo (clientes + documentos + líneas)
- Exportación selectiva (checkboxes)
- Importación con merge
- Progress tracking
### 8.4 Multi-Empresa
- Tabla `empresas` en BD
- Selector empresa activa
- Datos aislados por empresa_uuid
### 8.5 Templates Sistema
- Plantillas PDF personalizables
- Editor visual
- Vista previa en tiempo real
---
## 9. Widgets Third-Party (fynex-widgets)
**Librería vendorizada**: `/third_party/fynex-widgets/`
| Widget | Descripción | Uso |
|--------|-------------|-----|
| **AutoComplete** | Entry con dropdown sugerencias | Población, Provincia, País |
| **DateEntry** | Entry con calendar picker | Fechas |
| **NumEntry** | Entry solo números | Cantidades, precios |
| **SelectEntry** | Combo editable | Custom values |
| **Calendar** | Widget calendario | Selección fecha |
| **Tooltips** | Ayuda contextual | Hover |
---
## 10. Resumen de Requisitos para zCatGui
### 10.1 Widgets Necesarios (Orden Prioridad)
| # | Widget | Prioridad | Notas |
|---|--------|-----------|-------|
| 1 | **Table** | CRÍTICA | Edición in-situ, state indicators, sorting |
| 2 | **Input** | CRÍTICA | Autocomplete, validation |
| 3 | **Select** | CRÍTICA | Dropdown selection |
| 4 | **Panel** | ALTA | Título dinámico, bordes coloreados |
| 5 | **Split** | ALTA | HSplit/VSplit draggable |
| 6 | **Button** | ALTA | Disabled, importance levels |
| 7 | **Modal** | MEDIA | Diálogos modales |
| 8 | **List** | MEDIA | Lista seleccionable |
| 9 | **Checkbox** | MEDIA | Toggle boolean |
| 10 | **Label** | BAJA | Texto estático |
### 10.2 Layouts Necesarios
| Layout | Status |
|--------|--------|
| VBox/HBox | Usar Layout constraints |
| Grid | Usar Layout constraints |
| HSplit/VSplit | **CRÍTICO - Implementar** |
| Stack (Overlay) | Para popups |
| Border | Emular con constraints |
### 10.3 Features Críticos
1. **Table con edición in-situ** - 80% del trabajo
2. **Split panels draggables** - Layout principal
3. **Select/Dropdown** - 20+ lugares de uso
4. **Sistema de focus** - Navegación teclado
5. **Hot-reload themes** - Cambio colores en runtime
### 10.4 Estimación de Trabajo
| Componente | Líneas | Tiempo |
|------------|--------|--------|
| Table editable | 1500 | 2-3 semanas |
| Select/Dropdown | 300 | 3-4 días |
| Split layout | 200 | 2-3 días |
| Panel widget | 150 | 2 días |
| AutoComplete | 250 | 3-4 días |
| Modal/Dialog | 200 | 2-3 días |
| **TOTAL** | ~2600 | 4-5 semanas |
---
## 11. Ventajas de Portar a GUI Immediate-Mode
### Por qué Vale la Pena
1. **Sin fyne.Do()**: Código más simple, sin threading hell
2. **Performance**: Software rendering predecible
3. **Control total**: Sin "magia" del framework
4. **Testing**: Funciones puras, fácil de testear
5. **Debugging**: Estado visible, reproducible
### Comparación Threading
| Fyne (Retained) | zCatGui (Immediate) |
|-----------------|---------------------|
| 402 usos fyne.Do() | 0 equivalentes |
| Callbacks async | Polling síncrono |
| Estado oculto | Estado explícito |
| Race conditions | Sin races |
---
## 12. Archivos Clave Analizados
```
/cmd/simifactu/main.go (881 líneas)
/internal/ui/components/advanced_table/*.go (2000+ líneas)
/internal/ui/panels_v3/panels/who_detail/ui_layout.go (300 líneas)
/internal/ui/panels_v3/panels/document_detail/ui_layout.go (400 líneas)
/internal/ui/dialogs/import_window.go (500 líneas)
/third_party/fynex-widgets/autocomplete.go (600 líneas)
/internal/ui/panels_v3/data/manager_colors.go (800 líneas)
```
**Total analizado**: ~10,000 líneas de código + 5000 líneas de docs

81
examples/hello.zig Normal file
View file

@ -0,0 +1,81 @@
//! Hello World - Basic zCatGui example
//!
//! Demonstrates:
//! - Initializing the backend
//! - Creating a framebuffer
//! - Basic rendering
//! - Event loop
const std = @import("std");
const zcatgui = @import("zcatgui");
const Framebuffer = zcatgui.render.Framebuffer;
const SoftwareRenderer = zcatgui.render.SoftwareRenderer;
const Sdl2Backend = zcatgui.backend.Sdl2Backend;
const Color = zcatgui.Color;
const Command = zcatgui.Command;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Initialize backend
var backend = try Sdl2Backend.init("zCatGui - Hello World", 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);
var running = true;
var frame: u32 = 0;
while (running) {
// Poll events
while (backend.pollEvent()) |event| {
switch (event) {
.quit => running = false,
.key => |key| {
if (key.key == .escape and key.pressed) {
running = false;
}
},
.resize => |size| {
try fb.resize(size.width, size.height);
},
else => {},
}
}
// Clear
renderer.clear(Color.background);
// Draw some rectangles
renderer.execute(Command.rect(50, 50, 200, 100, Color.primary));
renderer.execute(Command.rect(300, 50, 200, 100, Color.success));
renderer.execute(Command.rect(550, 50, 200, 100, Color.danger));
// Draw rectangle outline
renderer.execute(Command.rectOutline(50, 200, 700, 100, Color.border));
// Animate a rectangle
const x = @as(i32, @intCast(50 + (frame % 600)));
renderer.execute(Command.rect(x, 350, 100, 100, Color.warning));
// Draw some lines
renderer.execute(Command.line(50, 500, 750, 500, Color.foreground));
renderer.execute(Command.line(400, 450, 400, 550, Color.foreground));
// Present
backend.present(&fb);
frame += 1;
// Cap at ~60 FPS
std.Thread.sleep(16 * std.time.ns_per_ms);
}
}

176
examples/macro_demo.zig Normal file
View file

@ -0,0 +1,176 @@
//! 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});
}

78
src/backend/backend.zig Normal file
View file

@ -0,0 +1,78 @@
//! Backend - Abstract interface for window/event backends
//!
//! The backend handles:
//! - Creating and managing the window
//! - Polling for input events
//! - Displaying the framebuffer
const std = @import("std");
const Input = @import("../core/input.zig");
const Framebuffer = @import("../render/framebuffer.zig").Framebuffer;
pub const KeyEvent = Input.KeyEvent;
pub const MouseEvent = Input.MouseEvent;
/// Event from the backend
pub const Event = union(enum) {
/// Keyboard event
key: KeyEvent,
/// Mouse event
mouse: MouseEvent,
/// Window resized
resize: struct {
width: u32,
height: u32,
},
/// Window close requested
quit,
/// Text input (Unicode)
text_input: struct {
text: [32]u8,
len: usize,
},
};
/// Abstract backend interface
pub const Backend = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
/// Poll for events (non-blocking)
pollEvent: *const fn (ptr: *anyopaque) ?Event,
/// Present the framebuffer to the screen
present: *const fn (ptr: *anyopaque, fb: *const Framebuffer) void,
/// Get window dimensions
getSize: *const fn (ptr: *anyopaque) struct { width: u32, height: u32 },
/// Clean up
deinit: *const fn (ptr: *anyopaque) void,
};
/// Poll for events
pub fn pollEvent(self: Backend) ?Event {
return self.vtable.pollEvent(self.ptr);
}
/// Present framebuffer
pub fn present(self: Backend, fb: *const Framebuffer) void {
self.vtable.present(self.ptr, fb);
}
/// Get window size
pub fn getSize(self: Backend) struct { width: u32, height: u32 } {
return self.vtable.getSize(self.ptr);
}
/// Clean up
pub fn deinit(self: Backend) void {
self.vtable.deinit(self.ptr);
}
};

323
src/backend/sdl2.zig Normal file
View file

@ -0,0 +1,323 @@
//! SDL2 Backend - Window and event handling via SDL2
//!
//! This is the primary backend for zCatGui.
//! SDL2 provides cross-platform window creation, event handling,
//! and texture-based rendering (we use it to display our software framebuffer).
const std = @import("std");
const c = @cImport({
@cInclude("SDL2/SDL.h");
});
const Backend = @import("backend.zig");
const Input = @import("../core/input.zig");
const Framebuffer = @import("../render/framebuffer.zig").Framebuffer;
const Event = Backend.Event;
const KeyEvent = Input.KeyEvent;
const Key = Input.Key;
const KeyModifiers = Input.KeyModifiers;
/// SDL2 backend implementation
pub const Sdl2Backend = struct {
window: *c.SDL_Window,
renderer: *c.SDL_Renderer,
texture: *c.SDL_Texture,
width: u32,
height: u32,
const Self = @This();
/// Initialize SDL2 and create a window
pub fn init(title: [*:0]const u8, width: u32, height: u32) !Self {
// Initialize SDL
if (c.SDL_Init(c.SDL_INIT_VIDEO) != 0) {
return error.SdlInitFailed;
}
errdefer c.SDL_Quit();
// Create window
const window = c.SDL_CreateWindow(
title,
c.SDL_WINDOWPOS_CENTERED,
c.SDL_WINDOWPOS_CENTERED,
@intCast(width),
@intCast(height),
c.SDL_WINDOW_SHOWN | c.SDL_WINDOW_RESIZABLE,
) orelse return error.WindowCreationFailed;
errdefer c.SDL_DestroyWindow(window);
// Create renderer (using software renderer for consistency)
const renderer = c.SDL_CreateRenderer(
window,
-1,
c.SDL_RENDERER_SOFTWARE,
) orelse return error.RendererCreationFailed;
errdefer c.SDL_DestroyRenderer(renderer);
// Create texture for framebuffer
const texture = c.SDL_CreateTexture(
renderer,
c.SDL_PIXELFORMAT_ABGR8888,
c.SDL_TEXTUREACCESS_STREAMING,
@intCast(width),
@intCast(height),
) orelse return error.TextureCreationFailed;
return Self{
.window = window,
.renderer = renderer,
.texture = texture,
.width = width,
.height = height,
};
}
/// Clean up SDL resources
pub fn deinit(self: *Self) void {
c.SDL_DestroyTexture(self.texture);
c.SDL_DestroyRenderer(self.renderer);
c.SDL_DestroyWindow(self.window);
c.SDL_Quit();
}
/// Poll for events (non-blocking)
pub fn pollEvent(self: *Self) ?Event {
_ = self;
var event: c.SDL_Event = undefined;
if (c.SDL_PollEvent(&event) == 0) {
return null;
}
return switch (event.type) {
c.SDL_QUIT => Event.quit,
c.SDL_KEYDOWN, c.SDL_KEYUP => blk: {
const pressed = event.type == c.SDL_KEYDOWN;
const key = translateKey(event.key.keysym.scancode);
const mods = translateModifiers(event.key.keysym.mod);
break :blk Event{
.key = .{
.key = key,
.modifiers = mods,
.char = null, // Filled by text input event
.pressed = pressed,
},
};
},
c.SDL_TEXTINPUT => blk: {
var text_event = Event{
.text_input = .{
.text = undefined,
.len = 0,
},
};
// Copy text (null-terminated)
var i: usize = 0;
while (i < 32 and event.text.text[i] != 0) : (i += 1) {
text_event.text_input.text[i] = @intCast(event.text.text[i]);
}
text_event.text_input.len = i;
break :blk text_event;
},
c.SDL_MOUSEMOTION => Event{
.mouse = .{
.x = event.motion.x,
.y = event.motion.y,
},
},
c.SDL_MOUSEBUTTONDOWN, c.SDL_MOUSEBUTTONUP => blk: {
const pressed = event.type == c.SDL_MOUSEBUTTONDOWN;
const button: ?Input.MouseButton = switch (event.button.button) {
c.SDL_BUTTON_LEFT => .left,
c.SDL_BUTTON_RIGHT => .right,
c.SDL_BUTTON_MIDDLE => .middle,
else => null,
};
break :blk Event{
.mouse = .{
.x = event.button.x,
.y = event.button.y,
.button = button,
.pressed = pressed,
},
};
},
c.SDL_MOUSEWHEEL => Event{
.mouse = .{
.x = 0,
.y = 0,
.scroll_x = event.wheel.x,
.scroll_y = event.wheel.y,
},
},
c.SDL_WINDOWEVENT => blk: {
if (event.window.event == c.SDL_WINDOWEVENT_RESIZED) {
break :blk Event{
.resize = .{
.width = @intCast(event.window.data1),
.height = @intCast(event.window.data2),
},
};
}
break :blk null;
},
else => null,
};
}
/// Present the framebuffer to the screen
pub fn present(self: *Self, fb: *const Framebuffer) void {
// Resize texture if needed
if (fb.width != self.width or fb.height != self.height) {
c.SDL_DestroyTexture(self.texture);
self.texture = c.SDL_CreateTexture(
self.renderer,
c.SDL_PIXELFORMAT_ABGR8888,
c.SDL_TEXTUREACCESS_STREAMING,
@intCast(fb.width),
@intCast(fb.height),
) orelse return;
self.width = fb.width;
self.height = fb.height;
}
// Update texture with framebuffer data
_ = c.SDL_UpdateTexture(
self.texture,
null,
fb.getData().ptr,
@intCast(fb.getPitch()),
);
// Render
_ = c.SDL_RenderClear(self.renderer);
_ = c.SDL_RenderCopy(self.renderer, self.texture, null, null);
c.SDL_RenderPresent(self.renderer);
}
/// Get window size
pub fn getSize(self: *Self) struct { width: u32, height: u32 } {
var w: c_int = 0;
var h: c_int = 0;
c.SDL_GetWindowSize(self.window, &w, &h);
return .{
.width = @intCast(w),
.height = @intCast(h),
};
}
/// Get as abstract Backend
pub fn backend(self: *Self) Backend.Backend {
return .{
.ptr = self,
.vtable = &vtable,
};
}
const vtable = Backend.Backend.VTable{
.pollEvent = @ptrCast(&pollEvent),
.present = @ptrCast(&present),
.getSize = @ptrCast(&getSize),
.deinit = @ptrCast(&deinit),
};
};
// =============================================================================
// Key translation
// =============================================================================
fn translateKey(scancode: c.SDL_Scancode) Key {
return switch (scancode) {
c.SDL_SCANCODE_A => .a,
c.SDL_SCANCODE_B => .b,
c.SDL_SCANCODE_C => .c,
c.SDL_SCANCODE_D => .d,
c.SDL_SCANCODE_E => .e,
c.SDL_SCANCODE_F => .f,
c.SDL_SCANCODE_G => .g,
c.SDL_SCANCODE_H => .h,
c.SDL_SCANCODE_I => .i,
c.SDL_SCANCODE_J => .j,
c.SDL_SCANCODE_K => .k,
c.SDL_SCANCODE_L => .l,
c.SDL_SCANCODE_M => .m,
c.SDL_SCANCODE_N => .n,
c.SDL_SCANCODE_O => .o,
c.SDL_SCANCODE_P => .p,
c.SDL_SCANCODE_Q => .q,
c.SDL_SCANCODE_R => .r,
c.SDL_SCANCODE_S => .s,
c.SDL_SCANCODE_T => .t,
c.SDL_SCANCODE_U => .u,
c.SDL_SCANCODE_V => .v,
c.SDL_SCANCODE_W => .w,
c.SDL_SCANCODE_X => .x,
c.SDL_SCANCODE_Y => .y,
c.SDL_SCANCODE_Z => .z,
c.SDL_SCANCODE_0 => .@"0",
c.SDL_SCANCODE_1 => .@"1",
c.SDL_SCANCODE_2 => .@"2",
c.SDL_SCANCODE_3 => .@"3",
c.SDL_SCANCODE_4 => .@"4",
c.SDL_SCANCODE_5 => .@"5",
c.SDL_SCANCODE_6 => .@"6",
c.SDL_SCANCODE_7 => .@"7",
c.SDL_SCANCODE_8 => .@"8",
c.SDL_SCANCODE_9 => .@"9",
c.SDL_SCANCODE_F1 => .f1,
c.SDL_SCANCODE_F2 => .f2,
c.SDL_SCANCODE_F3 => .f3,
c.SDL_SCANCODE_F4 => .f4,
c.SDL_SCANCODE_F5 => .f5,
c.SDL_SCANCODE_F6 => .f6,
c.SDL_SCANCODE_F7 => .f7,
c.SDL_SCANCODE_F8 => .f8,
c.SDL_SCANCODE_F9 => .f9,
c.SDL_SCANCODE_F10 => .f10,
c.SDL_SCANCODE_F11 => .f11,
c.SDL_SCANCODE_F12 => .f12,
c.SDL_SCANCODE_UP => .up,
c.SDL_SCANCODE_DOWN => .down,
c.SDL_SCANCODE_LEFT => .left,
c.SDL_SCANCODE_RIGHT => .right,
c.SDL_SCANCODE_HOME => .home,
c.SDL_SCANCODE_END => .end,
c.SDL_SCANCODE_PAGEUP => .page_up,
c.SDL_SCANCODE_PAGEDOWN => .page_down,
c.SDL_SCANCODE_BACKSPACE => .backspace,
c.SDL_SCANCODE_DELETE => .delete,
c.SDL_SCANCODE_INSERT => .insert,
c.SDL_SCANCODE_TAB => .tab,
c.SDL_SCANCODE_RETURN => .enter,
c.SDL_SCANCODE_ESCAPE => .escape,
c.SDL_SCANCODE_SPACE => .space,
c.SDL_SCANCODE_LSHIFT => .left_shift,
c.SDL_SCANCODE_RSHIFT => .right_shift,
c.SDL_SCANCODE_LCTRL => .left_ctrl,
c.SDL_SCANCODE_RCTRL => .right_ctrl,
c.SDL_SCANCODE_LALT => .left_alt,
c.SDL_SCANCODE_RALT => .right_alt,
else => .unknown,
};
}
fn translateModifiers(mod: u16) KeyModifiers {
return .{
.shift = (mod & c.KMOD_SHIFT) != 0,
.ctrl = (mod & c.KMOD_CTRL) != 0,
.alt = (mod & c.KMOD_ALT) != 0,
.super = (mod & c.KMOD_GUI) != 0,
};
}

159
src/core/command.zig Normal file
View file

@ -0,0 +1,159 @@
//! DrawCommand - Rendering commands for the software rasterizer
//!
//! The immediate mode UI generates a list of DrawCommands each frame.
//! The software rasterizer then processes these commands to produce pixels.
//!
//! This is the "command list" approach (like microui), not vertex buffers.
const Style = @import("style.zig");
/// A single draw command
pub const DrawCommand = union(enum) {
/// Draw a filled rectangle
rect: RectCommand,
/// Draw text
text: TextCommand,
/// Draw a line
line: LineCommand,
/// Draw a rectangle outline (border)
rect_outline: RectOutlineCommand,
/// Begin clipping to a rectangle
clip: ClipCommand,
/// End clipping
clip_end,
/// No operation (placeholder)
nop,
};
/// Draw a filled rectangle
pub const RectCommand = struct {
x: i32,
y: i32,
w: u32,
h: u32,
color: Style.Color,
};
/// Draw text at a position
pub const TextCommand = struct {
x: i32,
y: i32,
text: []const u8,
color: Style.Color,
/// null means default font
font: ?*anyopaque = null,
};
/// Draw a line between two points
pub const LineCommand = struct {
x1: i32,
y1: i32,
x2: i32,
y2: i32,
color: Style.Color,
};
/// Draw a rectangle outline
pub const RectOutlineCommand = struct {
x: i32,
y: i32,
w: u32,
h: u32,
color: Style.Color,
thickness: u32 = 1,
};
/// Begin clipping to a rectangle
pub const ClipCommand = struct {
x: i32,
y: i32,
w: u32,
h: u32,
};
// =============================================================================
// Helper constructors
// =============================================================================
/// Create a rect command
pub fn rect(x: i32, y: i32, w: u32, h: u32, color: Style.Color) DrawCommand {
return .{ .rect = .{
.x = x,
.y = y,
.w = w,
.h = h,
.color = color,
} };
}
/// Create a text command
pub fn text(x: i32, y: i32, str: []const u8, color: Style.Color) DrawCommand {
return .{ .text = .{
.x = x,
.y = y,
.text = str,
.color = color,
} };
}
/// Create a line command
pub fn line(x1: i32, y1: i32, x2: i32, y2: i32, color: Style.Color) DrawCommand {
return .{ .line = .{
.x1 = x1,
.y1 = y1,
.x2 = x2,
.y2 = y2,
.color = color,
} };
}
/// Create a rect outline command
pub fn rectOutline(x: i32, y: i32, w: u32, h: u32, color: Style.Color) DrawCommand {
return .{ .rect_outline = .{
.x = x,
.y = y,
.w = w,
.h = h,
.color = color,
} };
}
/// Create a clip command
pub fn clip(x: i32, y: i32, w: u32, h: u32) DrawCommand {
return .{ .clip = .{
.x = x,
.y = y,
.w = w,
.h = h,
} };
}
/// Create a clip end command
pub fn clipEnd() DrawCommand {
return .clip_end;
}
// =============================================================================
// Tests
// =============================================================================
const std = @import("std");
test "DrawCommand creation" {
const cmd = rect(10, 20, 100, 50, Style.Color.red);
switch (cmd) {
.rect => |r| {
try std.testing.expectEqual(@as(i32, 10), r.x);
try std.testing.expectEqual(@as(i32, 20), r.y);
try std.testing.expectEqual(@as(u32, 100), r.w);
try std.testing.expectEqual(@as(u32, 50), r.h);
},
else => unreachable,
}
}

161
src/core/context.zig Normal file
View file

@ -0,0 +1,161 @@
//! Context - Central state for immediate mode UI
//!
//! The Context holds all state needed for a frame:
//! - Input state (keyboard, mouse)
//! - Command list (draw commands)
//! - Layout state
//! - ID tracking for widgets
const std = @import("std");
const Allocator = std.mem.Allocator;
const Command = @import("command.zig");
const Input = @import("input.zig");
const Layout = @import("layout.zig");
const Style = @import("style.zig");
/// Central context for immediate mode UI
pub const Context = struct {
allocator: Allocator,
/// Draw commands for current frame
commands: std.ArrayListUnmanaged(Command.DrawCommand),
/// Input state
input: Input.InputState,
/// Layout state
layout: Layout.LayoutState,
/// ID stack for widget identification
id_stack: std.ArrayListUnmanaged(u32),
/// Current frame number
frame: u64,
/// Screen dimensions
width: u32,
height: u32,
const Self = @This();
/// Initialize a new context
pub fn init(allocator: Allocator, width: u32, height: u32) Self {
return .{
.allocator = allocator,
.commands = .{},
.input = Input.InputState.init(),
.layout = Layout.LayoutState.init(width, height),
.id_stack = .{},
.frame = 0,
.width = width,
.height = height,
};
}
/// Clean up resources
pub fn deinit(self: *Self) void {
self.commands.deinit(self.allocator);
self.id_stack.deinit(self.allocator);
}
/// Begin a new frame
pub fn beginFrame(self: *Self) void {
self.commands.clearRetainingCapacity();
self.id_stack.clearRetainingCapacity();
self.layout.reset(self.width, self.height);
self.frame += 1;
}
/// End the current frame
pub fn endFrame(self: *Self) void {
self.input.endFrame();
}
/// Get a unique ID for a widget
pub fn getId(self: *Self, label: []const u8) u32 {
var hash: u32 = 0;
// Include parent IDs
for (self.id_stack.items) |parent_id| {
hash = hashCombine(hash, parent_id);
}
// Hash the label
hash = hashCombine(hash, hashString(label));
return hash;
}
/// Push an ID onto the stack (for containers)
pub fn pushId(self: *Self, id: u32) void {
self.id_stack.append(self.allocator, id) catch {};
}
/// Pop an ID from the stack
pub fn popId(self: *Self) void {
_ = self.id_stack.pop();
}
/// Push a draw command
pub fn pushCommand(self: *Self, cmd: Command.DrawCommand) void {
self.commands.append(self.allocator, cmd) catch {};
}
/// Resize the context
pub fn resize(self: *Self, width: u32, height: u32) void {
self.width = width;
self.height = height;
}
// =========================================================================
// Helper functions
// =========================================================================
fn hashString(s: []const u8) u32 {
var h: u32 = 0;
for (s) |c| {
h = h *% 31 +% c;
}
return h;
}
fn hashCombine(a: u32, b: u32) u32 {
return a ^ (b +% 0x9e3779b9 +% (a << 6) +% (a >> 2));
}
};
// =============================================================================
// Tests
// =============================================================================
test "Context basic" {
var ctx = Context.init(std.testing.allocator, 800, 600);
defer ctx.deinit();
ctx.beginFrame();
const id1 = ctx.getId("button1");
const id2 = ctx.getId("button2");
try std.testing.expect(id1 != id2);
ctx.endFrame();
}
test "Context ID with parent" {
var ctx = Context.init(std.testing.allocator, 800, 600);
defer ctx.deinit();
ctx.beginFrame();
const id_no_parent = ctx.getId("button");
ctx.pushId(ctx.getId("panel1"));
const id_with_parent = ctx.getId("button");
ctx.popId();
try std.testing.expect(id_no_parent != id_with_parent);
ctx.endFrame();
}

299
src/core/input.zig Normal file
View file

@ -0,0 +1,299 @@
//! Input - Keyboard and mouse input state
//!
//! Tracks the current state of input devices.
//! Updated by the backend each frame.
const std = @import("std");
/// Key codes (subset, extend as needed)
pub const Key = enum(u16) {
// Letters
a,
b,
c,
d,
e,
f,
g,
h,
i,
j,
k,
l,
m,
n,
o,
p,
q,
r,
s,
t,
u,
v,
w,
x,
y,
z,
// Numbers
@"0",
@"1",
@"2",
@"3",
@"4",
@"5",
@"6",
@"7",
@"8",
@"9",
// Function keys
f1,
f2,
f3,
f4,
f5,
f6,
f7,
f8,
f9,
f10,
f11,
f12,
// Navigation
up,
down,
left,
right,
home,
end,
page_up,
page_down,
// Editing
backspace,
delete,
insert,
tab,
enter,
escape,
space,
// Modifiers (as keys)
left_shift,
right_shift,
left_ctrl,
right_ctrl,
left_alt,
right_alt,
// Punctuation
minus,
equals,
left_bracket,
right_bracket,
backslash,
semicolon,
apostrophe,
grave,
comma,
period,
slash,
// Unknown
unknown,
_,
};
/// Key modifiers
pub const KeyModifiers = packed struct {
shift: bool = false,
ctrl: bool = false,
alt: bool = false,
super: bool = false,
pub const none = KeyModifiers{};
};
/// A keyboard event
pub const KeyEvent = struct {
key: Key,
modifiers: KeyModifiers,
/// The character produced (if any)
char: ?u21 = null,
/// True if key was pressed, false if released
pressed: bool,
/// Check if this is a printable character
pub fn isPrintable(self: KeyEvent) bool {
return self.char != null and self.char.? >= 32;
}
/// Get the character as a slice (for convenience)
pub fn charAsSlice(self: KeyEvent, buf: *[4]u8) ?[]const u8 {
if (self.char) |c| {
const len = std.unicode.utf8Encode(c, buf) catch return null;
return buf[0..len];
}
return null;
}
};
/// Mouse buttons
pub const MouseButton = enum {
left,
right,
middle,
x1,
x2,
};
/// A mouse event
pub const MouseEvent = struct {
x: i32,
y: i32,
button: ?MouseButton = null,
pressed: bool = false,
scroll_x: i32 = 0,
scroll_y: i32 = 0,
};
/// Current input state
pub const InputState = struct {
// Mouse position
mouse_x: i32 = 0,
mouse_y: i32 = 0,
// Mouse buttons (current frame)
mouse_down: [5]bool = .{ false, false, false, false, false },
// Mouse buttons (previous frame, for detecting clicks)
mouse_down_prev: [5]bool = .{ false, false, false, false, false },
// Scroll delta
scroll_x: i32 = 0,
scroll_y: i32 = 0,
// Key modifiers
modifiers: KeyModifiers = .{},
// Text input this frame
text_input: [64]u8 = undefined,
text_input_len: usize = 0,
const Self = @This();
/// Initialize input state
pub fn init() Self {
return .{};
}
/// Call at end of frame to prepare for next
pub fn endFrame(self: *Self) void {
self.mouse_down_prev = self.mouse_down;
self.scroll_x = 0;
self.scroll_y = 0;
self.text_input_len = 0;
}
/// Update mouse position
pub fn setMousePos(self: *Self, x: i32, y: i32) void {
self.mouse_x = x;
self.mouse_y = y;
}
/// Update mouse button state
pub fn setMouseButton(self: *Self, button: MouseButton, pressed: bool) void {
self.mouse_down[@intFromEnum(button)] = pressed;
}
/// Add scroll delta
pub fn addScroll(self: *Self, x: i32, y: i32) void {
self.scroll_x += x;
self.scroll_y += y;
}
/// Update key modifiers
pub fn setModifiers(self: *Self, mods: KeyModifiers) void {
self.modifiers = mods;
}
/// Add text input
pub fn addTextInput(self: *Self, text: []const u8) void {
const remaining = self.text_input.len - self.text_input_len;
const to_copy = @min(text.len, remaining);
@memcpy(self.text_input[self.text_input_len..][0..to_copy], text[0..to_copy]);
self.text_input_len += to_copy;
}
// =========================================================================
// Query functions
// =========================================================================
/// Get current mouse position
pub fn mousePos(self: Self) struct { x: i32, y: i32 } {
return .{ .x = self.mouse_x, .y = self.mouse_y };
}
/// Check if mouse button is currently down
pub fn mouseDown(self: Self, button: MouseButton) bool {
return self.mouse_down[@intFromEnum(button)];
}
/// Check if mouse button was just pressed this frame
pub fn mousePressed(self: Self, button: MouseButton) bool {
const idx = @intFromEnum(button);
return self.mouse_down[idx] and !self.mouse_down_prev[idx];
}
/// Check if mouse button was just released this frame
pub fn mouseReleased(self: Self, button: MouseButton) bool {
const idx = @intFromEnum(button);
return !self.mouse_down[idx] and self.mouse_down_prev[idx];
}
/// Get text input this frame
pub fn getTextInput(self: Self) []const u8 {
return self.text_input[0..self.text_input_len];
}
};
// =============================================================================
// Tests
// =============================================================================
test "InputState mouse" {
var input = InputState.init();
input.setMousePos(100, 200);
try std.testing.expectEqual(@as(i32, 100), input.mouse_x);
try std.testing.expectEqual(@as(i32, 200), input.mouse_y);
input.setMouseButton(.left, true);
try std.testing.expect(input.mouseDown(.left));
try std.testing.expect(input.mousePressed(.left));
input.endFrame();
try std.testing.expect(input.mouseDown(.left));
try std.testing.expect(!input.mousePressed(.left));
input.setMouseButton(.left, false);
try std.testing.expect(input.mouseReleased(.left));
}
test "KeyEvent char" {
var buf: [4]u8 = undefined;
const event = KeyEvent{
.key = .a,
.modifiers = .{},
.char = 'A',
.pressed = true,
};
const slice = event.charAsSlice(&buf);
try std.testing.expect(slice != null);
try std.testing.expectEqualStrings("A", slice.?);
}

408
src/core/layout.zig Normal file
View file

@ -0,0 +1,408 @@
//! Layout - Constraint-based layout system
//!
//! Based on zcatui's layout system, adapted for GUI.
//! Provides Rect, Constraint, and layout calculations.
const std = @import("std");
/// A rectangle (position + size)
pub const Rect = struct {
x: i32,
y: i32,
w: u32,
h: u32,
const Self = @This();
/// Create a new rectangle
pub fn init(x: i32, y: i32, w: u32, h: u32) Self {
return .{ .x = x, .y = y, .w = w, .h = h };
}
/// Create a zero-size rectangle
pub fn zero() Self {
return .{ .x = 0, .y = 0, .w = 0, .h = 0 };
}
/// Check if the rectangle is empty
pub fn isEmpty(self: Self) bool {
return self.w == 0 or self.h == 0;
}
/// Get the area
pub fn area(self: Self) u32 {
return self.w * self.h;
}
/// Get left edge
pub fn left(self: Self) i32 {
return self.x;
}
/// Get right edge
pub fn right(self: Self) i32 {
return self.x + @as(i32, @intCast(self.w));
}
/// Get top edge
pub fn top(self: Self) i32 {
return self.y;
}
/// Get bottom edge
pub fn bottom(self: Self) i32 {
return self.y + @as(i32, @intCast(self.h));
}
/// Get center point
pub fn center(self: Self) struct { x: i32, y: i32 } {
return .{
.x = self.x + @as(i32, @intCast(self.w / 2)),
.y = self.y + @as(i32, @intCast(self.h / 2)),
};
}
/// Check if a point is inside the rectangle
pub fn contains(self: Self, px: i32, py: i32) bool {
return px >= self.x and
px < self.x + @as(i32, @intCast(self.w)) and
py >= self.y and
py < self.y + @as(i32, @intCast(self.h));
}
/// Get intersection with another rectangle
pub fn intersection(self: Self, other: Self) Self {
const x1 = @max(self.x, other.x);
const y1 = @max(self.y, other.y);
const x2 = @min(self.right(), other.right());
const y2 = @min(self.bottom(), other.bottom());
if (x2 <= x1 or y2 <= y1) {
return Rect.zero();
}
return Rect.init(x1, y1, @intCast(x2 - x1), @intCast(y2 - y1));
}
/// Shrink by margin on all sides
pub fn shrink(self: Self, margin: u32) Self {
const m = @as(i32, @intCast(margin));
const m2 = margin * 2;
if (self.w <= m2 or self.h <= m2) {
return Rect.zero();
}
return Rect.init(
self.x + m,
self.y + m,
self.w - m2,
self.h - m2,
);
}
/// Shrink with different margins
pub fn shrinkSides(self: Self, top_val: u32, right_val: u32, bottom_val: u32, left_val: u32) Self {
const t = @as(i32, @intCast(top_val));
const r = right_val;
const b = bottom_val;
const l = @as(i32, @intCast(left_val));
const new_w = if (self.w > l + r) self.w - @as(u32, @intCast(l)) - r else 0;
const new_h = if (self.h > t + b) self.h - @as(u32, @intCast(t)) - b else 0;
return Rect.init(
self.x + l,
self.y + t,
new_w,
new_h,
);
}
};
/// A layout constraint
pub const Constraint = union(enum) {
/// Fixed size in pixels
length: u32,
/// Minimum size
min: u32,
/// Maximum size
max: u32,
/// Percentage of available space (0-100)
percentage: u8,
/// Ratio (numerator, denominator)
ratio: struct { num: u16, den: u16 },
/// Fill remaining space (weight)
fill: u16,
const Self = @This();
/// Create a fixed length constraint
pub fn len(val: u32) Self {
return .{ .length = val };
}
/// Create a minimum constraint
pub fn minSize(val: u32) Self {
return .{ .min = val };
}
/// Create a maximum constraint
pub fn maxSize(val: u32) Self {
return .{ .max = val };
}
/// Create a percentage constraint
pub fn pct(val: u8) Self {
return .{ .percentage = val };
}
/// Create a ratio constraint
pub fn rat(num: u16, den: u16) Self {
return .{ .ratio = .{ .num = num, .den = den } };
}
/// Create a fill constraint with weight
pub fn fillSpace() Self {
return .{ .fill = 1 };
}
/// Create a fill constraint with specific weight
pub fn fillWeight(weight: u16) Self {
return .{ .fill = weight };
}
// Convenience methods
pub fn half() Self {
return pct(50);
}
pub fn third() Self {
return rat(1, 3);
}
pub fn quarter() Self {
return pct(25);
}
};
/// Direction for layouts
pub const Direction = enum {
horizontal,
vertical,
};
/// Layout state for managing widget positions
pub const LayoutState = struct {
/// Current cursor position
cursor_x: i32,
cursor_y: i32,
/// Available area
area: Rect,
/// Row/column layout state
row_height: u32,
col_widths: [16]u32,
col_count: usize,
col_index: usize,
const Self = @This();
/// Initialize layout state
pub fn init(width: u32, height: u32) Self {
return .{
.cursor_x = 0,
.cursor_y = 0,
.area = Rect.init(0, 0, width, height),
.row_height = 0,
.col_widths = [_]u32{0} ** 16,
.col_count = 0,
.col_index = 0,
};
}
/// Reset to initial state
pub fn reset(self: *Self, width: u32, height: u32) void {
self.cursor_x = 0;
self.cursor_y = 0;
self.area = Rect.init(0, 0, width, height);
self.row_height = 0;
self.col_count = 0;
self.col_index = 0;
}
/// Begin a row layout
pub fn beginRow(self: *Self, height: u32, widths: []const u32) void {
self.row_height = height;
self.col_index = 0;
self.col_count = @min(widths.len, self.col_widths.len);
for (widths, 0..) |w, i| {
if (i >= self.col_widths.len) break;
self.col_widths[i] = w;
}
}
/// End the current row
pub fn endRow(self: *Self) void {
self.cursor_y += @as(i32, @intCast(self.row_height));
self.cursor_x = self.area.x;
self.col_index = 0;
}
/// Get the next rectangle in the current layout
pub fn nextRect(self: *Self) Rect {
if (self.col_count == 0) {
// No row layout active, use full width
const rect = Rect.init(
self.cursor_x,
self.cursor_y,
self.area.w,
self.row_height,
);
self.cursor_y += @as(i32, @intCast(self.row_height));
return rect;
}
// Column layout
if (self.col_index >= self.col_count) {
self.endRow();
}
const width = self.col_widths[self.col_index];
const rect = Rect.init(
self.cursor_x,
self.cursor_y,
width,
self.row_height,
);
self.cursor_x += @as(i32, @intCast(width));
self.col_index += 1;
return rect;
}
};
// =============================================================================
// Layout helpers
// =============================================================================
/// Split an area vertically
pub fn splitVertical(area: Rect, constraints: []const Constraint) [16]Rect {
var results: [16]Rect = undefined;
var remaining = area.h;
var y = area.y;
// First pass: calculate fixed sizes
var total_fill: u32 = 0;
for (constraints, 0..) |c, i| {
if (i >= results.len) break;
switch (c) {
.length => |len| remaining -|= len,
.percentage => |pct| remaining -|= (area.h * pct) / 100,
.fill => |w| total_fill += w,
else => {},
}
}
// Second pass: assign rectangles
for (constraints, 0..) |c, i| {
if (i >= results.len) break;
const height: u32 = switch (c) {
.length => |len| len,
.percentage => |pct| (area.h * pct) / 100,
.fill => |w| if (total_fill > 0) (remaining * w) / total_fill else 0,
.min => |min_val| @max(min_val, remaining),
.max => |max_val| @min(max_val, remaining),
.ratio => |r| if (r.den > 0) (area.h * r.num) / r.den else 0,
};
results[i] = Rect.init(area.x, y, area.w, height);
y += @as(i32, @intCast(height));
}
return results;
}
/// Split an area horizontally
pub fn splitHorizontal(area: Rect, constraints: []const Constraint) [16]Rect {
var results: [16]Rect = undefined;
var remaining = area.w;
var x = area.x;
// First pass: calculate fixed sizes
var total_fill: u32 = 0;
for (constraints, 0..) |c, i| {
if (i >= results.len) break;
switch (c) {
.length => |len| remaining -|= len,
.percentage => |pct| remaining -|= (area.w * pct) / 100,
.fill => |w| total_fill += w,
else => {},
}
}
// Second pass: assign rectangles
for (constraints, 0..) |c, i| {
if (i >= results.len) break;
const width: u32 = switch (c) {
.length => |len| len,
.percentage => |pct| (area.w * pct) / 100,
.fill => |w| if (total_fill > 0) (remaining * w) / total_fill else 0,
.min => |min_val| @max(min_val, remaining),
.max => |max_val| @min(max_val, remaining),
.ratio => |r| if (r.den > 0) (area.w * r.num) / r.den else 0,
};
results[i] = Rect.init(x, area.y, width, area.h);
x += @as(i32, @intCast(width));
}
return results;
}
// =============================================================================
// Tests
// =============================================================================
test "Rect contains" {
const rect = Rect.init(10, 20, 100, 50);
try std.testing.expect(rect.contains(50, 40));
try std.testing.expect(!rect.contains(5, 40));
try std.testing.expect(!rect.contains(50, 100));
}
test "Rect shrink" {
const rect = Rect.init(0, 0, 100, 100);
const shrunk = rect.shrink(10);
try std.testing.expectEqual(@as(i32, 10), shrunk.x);
try std.testing.expectEqual(@as(i32, 10), shrunk.y);
try std.testing.expectEqual(@as(u32, 80), shrunk.w);
try std.testing.expectEqual(@as(u32, 80), shrunk.h);
}
test "splitVertical" {
const area = Rect.init(0, 0, 100, 100);
const constraints = [_]Constraint{
Constraint.len(20),
Constraint.fillSpace(),
Constraint.len(20),
};
const results = splitVertical(area, &constraints);
try std.testing.expectEqual(@as(u32, 20), results[0].h);
try std.testing.expectEqual(@as(u32, 60), results[1].h);
try std.testing.expectEqual(@as(u32, 20), results[2].h);
}

244
src/core/style.zig Normal file
View file

@ -0,0 +1,244 @@
//! Style - Colors and visual styling
//!
//! Based on zcatui's style system, adapted for GUI with RGBA colors.
const std = @import("std");
/// RGBA Color
pub const Color = struct {
r: u8,
g: u8,
b: u8,
a: u8 = 255,
const Self = @This();
/// Create a color from RGB values
pub fn rgb(r: u8, g: u8, b: u8) Self {
return .{ .r = r, .g = g, .b = b, .a = 255 };
}
/// Create a color from RGBA values
pub fn rgba(r: u8, g: u8, b: u8, a: u8) Self {
return .{ .r = r, .g = g, .b = b, .a = a };
}
/// Convert to u32 (RGBA format)
pub fn toU32(self: Self) u32 {
return (@as(u32, self.r) << 24) |
(@as(u32, self.g) << 16) |
(@as(u32, self.b) << 8) |
@as(u32, self.a);
}
/// Convert to u32 (ABGR format for SDL)
pub fn toABGR(self: Self) u32 {
return (@as(u32, self.a) << 24) |
(@as(u32, self.b) << 16) |
(@as(u32, self.g) << 8) |
@as(u32, self.r);
}
/// Blend this color over another
pub fn blend(self: Self, bg_color: Self) Self {
if (self.a == 255) return self;
if (self.a == 0) return bg_color;
const alpha = @as(u16, self.a);
const inv_alpha = 255 - alpha;
return .{
.r = @intCast((@as(u16, self.r) * alpha + @as(u16, bg_color.r) * inv_alpha) / 255),
.g = @intCast((@as(u16, self.g) * alpha + @as(u16, bg_color.g) * inv_alpha) / 255),
.b = @intCast((@as(u16, self.b) * alpha + @as(u16, bg_color.b) * inv_alpha) / 255),
.a = 255,
};
}
/// Darken color by percentage (0-100)
pub fn darken(self: Self, percent: u8) Self {
const factor = @as(u16, 100 - @min(percent, 100));
return .{
.r = @intCast((@as(u16, self.r) * factor) / 100),
.g = @intCast((@as(u16, self.g) * factor) / 100),
.b = @intCast((@as(u16, self.b) * factor) / 100),
.a = self.a,
};
}
/// Lighten color by percentage (0-100)
pub fn lighten(self: Self, percent: u8) Self {
const factor = @as(u16, @min(percent, 100));
return .{
.r = @intCast(@as(u16, self.r) + ((@as(u16, 255) - self.r) * factor) / 100),
.g = @intCast(@as(u16, self.g) + ((@as(u16, 255) - self.g) * factor) / 100),
.b = @intCast(@as(u16, self.b) + ((@as(u16, 255) - self.b) * factor) / 100),
.a = self.a,
};
}
// =========================================================================
// Predefined colors
// =========================================================================
pub const transparent = Color.rgba(0, 0, 0, 0);
pub const black = Color.rgb(0, 0, 0);
pub const white = Color.rgb(255, 255, 255);
pub const red = Color.rgb(255, 0, 0);
pub const green = Color.rgb(0, 255, 0);
pub const blue = Color.rgb(0, 0, 255);
pub const yellow = Color.rgb(255, 255, 0);
pub const cyan = Color.rgb(0, 255, 255);
pub const magenta = Color.rgb(255, 0, 255);
pub const gray = Color.rgb(128, 128, 128);
pub const dark_gray = Color.rgb(64, 64, 64);
pub const light_gray = Color.rgb(192, 192, 192);
// UI colors
pub const background = Color.rgb(30, 30, 30);
pub const foreground = Color.rgb(220, 220, 220);
pub const primary = Color.rgb(66, 135, 245);
pub const secondary = Color.rgb(100, 100, 100);
pub const success = Color.rgb(76, 175, 80);
pub const warning = Color.rgb(255, 152, 0);
pub const danger = Color.rgb(244, 67, 54);
pub const border = Color.rgb(80, 80, 80);
};
/// Visual style for widgets
pub const Style = struct {
foreground: Color = Color.foreground,
background: Color = Color.background,
border: ?Color = null,
border_radius: u8 = 0,
const Self = @This();
/// Set foreground color
pub fn fg(self: Self, color: Color) Self {
var s = self;
s.foreground = color;
return s;
}
/// Set background color
pub fn bg(self: Self, color: Color) Self {
var s = self;
s.background = color;
return s;
}
/// Set border color
pub fn withBorder(self: Self, color: Color) Self {
var s = self;
s.border = color;
return s;
}
};
// =============================================================================
// Theme
// =============================================================================
/// A theme defines colors for all UI elements
pub const Theme = struct {
background: Color,
foreground: Color,
primary: Color,
secondary: Color,
success: Color,
warning: Color,
danger: Color,
border: Color,
// Widget-specific
button_bg: Color,
button_fg: Color,
button_hover: Color,
button_active: Color,
input_bg: Color,
input_fg: Color,
input_border: Color,
selection_bg: Color,
selection_fg: Color,
const Self = @This();
/// Dark theme (default)
pub const dark = Self{
.background = Color.rgb(30, 30, 30),
.foreground = Color.rgb(220, 220, 220),
.primary = Color.rgb(66, 135, 245),
.secondary = Color.rgb(100, 100, 100),
.success = Color.rgb(76, 175, 80),
.warning = Color.rgb(255, 152, 0),
.danger = Color.rgb(244, 67, 54),
.border = Color.rgb(80, 80, 80),
.button_bg = Color.rgb(60, 60, 60),
.button_fg = Color.rgb(220, 220, 220),
.button_hover = Color.rgb(80, 80, 80),
.button_active = Color.rgb(50, 50, 50),
.input_bg = Color.rgb(45, 45, 45),
.input_fg = Color.rgb(220, 220, 220),
.input_border = Color.rgb(80, 80, 80),
.selection_bg = Color.rgb(66, 135, 245),
.selection_fg = Color.rgb(255, 255, 255),
};
/// Light theme
pub const light = Self{
.background = Color.rgb(245, 245, 245),
.foreground = Color.rgb(30, 30, 30),
.primary = Color.rgb(33, 150, 243),
.secondary = Color.rgb(158, 158, 158),
.success = Color.rgb(76, 175, 80),
.warning = Color.rgb(255, 152, 0),
.danger = Color.rgb(244, 67, 54),
.border = Color.rgb(200, 200, 200),
.button_bg = Color.rgb(230, 230, 230),
.button_fg = Color.rgb(30, 30, 30),
.button_hover = Color.rgb(210, 210, 210),
.button_active = Color.rgb(190, 190, 190),
.input_bg = Color.rgb(255, 255, 255),
.input_fg = Color.rgb(30, 30, 30),
.input_border = Color.rgb(180, 180, 180),
.selection_bg = Color.rgb(33, 150, 243),
.selection_fg = Color.rgb(255, 255, 255),
};
};
// =============================================================================
// Tests
// =============================================================================
test "Color creation" {
const c = Color.rgb(100, 150, 200);
try std.testing.expectEqual(@as(u8, 100), c.r);
try std.testing.expectEqual(@as(u8, 150), c.g);
try std.testing.expectEqual(@as(u8, 200), c.b);
try std.testing.expectEqual(@as(u8, 255), c.a);
}
test "Color darken" {
const white = Color.white;
const darkened = white.darken(50);
try std.testing.expectEqual(@as(u8, 127), darkened.r);
}
test "Color blend" {
const fg = Color.rgba(255, 0, 0, 128);
const bg = Color.rgb(0, 0, 255);
const blended = fg.blend(bg);
// Should be purple-ish
try std.testing.expect(blended.r > 100);
try std.testing.expect(blended.b > 100);
}

340
src/macro/macro.zig Normal file
View file

@ -0,0 +1,340 @@
//! Macro System - Record and playback user actions
//!
//! The macro system is a CORNERSTONE of zCatGui.
//! It allows recording raw keyboard input and replaying it exactly.
//!
//! ## Design Decision: Raw Keys, Not Commands
//!
//! We record raw key events, not semantic commands. This is:
//! - Simpler (less code = less bugs)
//! - Like Vim (proven to work)
//! - Minimal memory usage
//!
//! ## Mouse Handling Strategy
//!
//! Phase 1: Keyboard only
//! Phase 2: Mouse clicks translated to equivalent keyboard actions
//!
//! Example: Click on button "Save" Tab to focus + Enter
//!
//! This works because our UI is fully keyboard-navigable.
const std = @import("std");
const Allocator = std.mem.Allocator;
const Input = @import("../core/input.zig");
pub const KeyEvent = Input.KeyEvent;
pub const Key = Input.Key;
pub const KeyModifiers = Input.KeyModifiers;
/// A recorded macro (sequence of key events)
pub const Macro = struct {
name: []const u8,
events: []const KeyEvent,
allocator: Allocator,
const Self = @This();
/// Create a new macro from events
pub fn init(allocator: Allocator, name: []const u8, events: []const KeyEvent) !Self {
const name_copy = try allocator.dupe(u8, name);
const events_copy = try allocator.dupe(KeyEvent, events);
return .{
.name = name_copy,
.events = events_copy,
.allocator = allocator,
};
}
/// Free the macro
pub fn deinit(self: Self) void {
self.allocator.free(self.name);
self.allocator.free(self.events);
}
};
/// Records key events into a macro
pub const MacroRecorder = struct {
allocator: Allocator,
events: std.ArrayListUnmanaged(KeyEvent),
recording: bool,
const Self = @This();
/// Initialize the recorder
pub fn init(allocator: Allocator) Self {
return .{
.allocator = allocator,
.events = .{},
.recording = false,
};
}
/// Clean up
pub fn deinit(self: *Self) void {
self.events.deinit(self.allocator);
}
/// Start recording
pub fn start(self: *Self) void {
self.events.clearRetainingCapacity();
self.recording = true;
}
/// Stop recording and return the events
pub fn stop(self: *Self) []const KeyEvent {
self.recording = false;
return self.events.items;
}
/// Record a key event (if recording is active)
pub fn record(self: *Self, event: KeyEvent) void {
if (self.recording) {
// Only record key presses, not releases (like Vim)
if (event.pressed) {
self.events.append(self.allocator, event) catch {};
}
}
}
/// Check if currently recording
pub fn isRecording(self: Self) bool {
return self.recording;
}
/// Get number of recorded events
pub fn eventCount(self: Self) usize {
return self.events.items.len;
}
/// Create a Macro from the recorded events
pub fn toMacro(self: *Self, name: []const u8) !Macro {
return Macro.init(self.allocator, name, self.events.items);
}
/// Save recorded events to a file
pub fn save(self: *Self, path: []const u8) !void {
const file = try std.fs.cwd().createFile(path, .{});
defer file.close();
// Write header directly
_ = try file.write("ZCATGUI_MACRO_V1\n");
// Write events
var line_buf: [128]u8 = undefined;
for (self.events.items) |event| {
const mods: u4 = @bitCast(event.modifiers);
const line = std.fmt.bufPrint(&line_buf, "{d},{d},{d},{d}\n", .{
@intFromEnum(event.key),
@as(u8, mods),
event.char orelse 0,
@as(u8, if (event.pressed) 1 else 0),
}) catch continue;
_ = try file.write(line);
}
}
/// Load events from a file
pub fn load(self: *Self, path: []const u8) !void {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
// Read file content into buffer
const stat = try file.stat();
const content = try self.allocator.alloc(u8, stat.size);
defer self.allocator.free(content);
_ = try file.readAll(content);
// Parse content line by line
var lines = std.mem.splitScalar(u8, content, '\n');
// Verify header
const header = lines.next() orelse return error.InvalidMacroFile;
if (!std.mem.eql(u8, header, "ZCATGUI_MACRO_V1")) {
return error.InvalidMacroFile;
}
self.events.clearRetainingCapacity();
// Read events
while (lines.next()) |line| {
if (line.len == 0) continue;
var iter = std.mem.splitScalar(u8, line, ',');
const key_int = std.fmt.parseInt(u16, iter.next() orelse continue, 10) catch continue;
const mods_int = std.fmt.parseInt(u4, iter.next() orelse continue, 10) catch continue;
const char_int = std.fmt.parseInt(u21, iter.next() orelse continue, 10) catch continue;
const pressed_int = std.fmt.parseInt(u8, iter.next() orelse continue, 10) catch continue;
try self.events.append(self.allocator, .{
.key = @enumFromInt(key_int),
.modifiers = @bitCast(mods_int),
.char = if (char_int == 0) null else char_int,
.pressed = pressed_int != 0,
});
}
}
};
/// Plays back recorded key events
pub const MacroPlayer = struct {
/// Function type for injecting events
pub const InjectFn = *const fn (KeyEvent) void;
/// Play a macro by injecting events
pub fn play(
events: []const KeyEvent,
inject_fn: InjectFn,
) void {
for (events) |event| {
inject_fn(event);
}
}
/// Play with delay between events (for visualization)
pub fn playWithDelay(
events: []const KeyEvent,
inject_fn: InjectFn,
delay_ms: u64,
) void {
for (events) |event| {
inject_fn(event);
if (delay_ms > 0) {
std.Thread.sleep(delay_ms * std.time.ns_per_ms);
}
}
}
/// Play a Macro struct
pub fn playMacro(macro_data: Macro, inject_fn: InjectFn) void {
play(macro_data.events, inject_fn);
}
};
/// Macro storage for managing multiple macros
pub const MacroStorage = struct {
allocator: Allocator,
macros: std.StringHashMap(Macro),
const Self = @This();
pub fn init(allocator: Allocator) Self {
return .{
.allocator = allocator,
.macros = std.StringHashMap(Macro).init(allocator),
};
}
pub fn deinit(self: *Self) void {
var iter = self.macros.valueIterator();
while (iter.next()) |macro_ptr| {
macro_ptr.deinit();
}
self.macros.deinit();
}
/// Store a macro
pub fn store(self: *Self, macro_data: Macro) !void {
// Remove existing if present
if (self.macros.fetchRemove(macro_data.name)) |removed| {
removed.value.deinit();
}
try self.macros.put(macro_data.name, macro_data);
}
/// Get a macro by name
pub fn get(self: *Self, name: []const u8) ?*const Macro {
return self.macros.getPtr(name);
}
/// Remove a macro
pub fn remove(self: *Self, name: []const u8) void {
if (self.macros.fetchRemove(name)) |removed| {
removed.value.deinit();
}
}
/// List all macro names
pub fn list(self: *Self) [][]const u8 {
var names = std.ArrayList([]const u8).init(self.allocator);
var iter = self.macros.keyIterator();
while (iter.next()) |key| {
names.append(key.*) catch {};
}
return names.toOwnedSlice() catch &[_][]const u8{};
}
};
// =============================================================================
// Tests
// =============================================================================
test "MacroRecorder basic" {
var recorder = MacroRecorder.init(std.testing.allocator);
defer recorder.deinit();
try std.testing.expect(!recorder.isRecording());
recorder.start();
try std.testing.expect(recorder.isRecording());
recorder.record(.{ .key = .a, .modifiers = .{}, .char = 'a', .pressed = true });
recorder.record(.{ .key = .b, .modifiers = .{}, .char = 'b', .pressed = true });
recorder.record(.{ .key = .c, .modifiers = .{}, .char = 'c', .pressed = false }); // Release, ignored
const events = recorder.stop();
try std.testing.expect(!recorder.isRecording());
try std.testing.expectEqual(@as(usize, 2), events.len);
}
test "MacroRecorder to Macro" {
var recorder = MacroRecorder.init(std.testing.allocator);
defer recorder.deinit();
recorder.start();
recorder.record(.{ .key = .a, .modifiers = .{}, .char = 'a', .pressed = true });
_ = recorder.stop();
const macro_data = try recorder.toMacro("test_macro");
defer macro_data.deinit();
try std.testing.expectEqualStrings("test_macro", macro_data.name);
try std.testing.expectEqual(@as(usize, 1), macro_data.events.len);
}
test "MacroPlayer" {
const events = [_]KeyEvent{
.{ .key = .a, .modifiers = .{}, .char = 'a', .pressed = true },
.{ .key = .b, .modifiers = .{}, .char = 'b', .pressed = true },
};
const inject = struct {
fn f(_: KeyEvent) void {
// In real code, this would inject into the input system
}
}.f;
MacroPlayer.play(&events, inject);
// Just verify it runs without crashing
}
test "MacroStorage" {
var storage = MacroStorage.init(std.testing.allocator);
defer storage.deinit();
const macro1 = try Macro.init(std.testing.allocator, "macro1", &[_]KeyEvent{});
try storage.store(macro1);
const retrieved = storage.get("macro1");
try std.testing.expect(retrieved != null);
try std.testing.expectEqualStrings("macro1", retrieved.?.name);
storage.remove("macro1");
try std.testing.expect(storage.get("macro1") == null);
}

207
src/render/font.zig Normal file
View file

@ -0,0 +1,207 @@
//! Font - Bitmap font rendering
//!
//! Simple bitmap font for basic text rendering.
//! TTF support can be added later via stb_truetype.
const std = @import("std");
const Style = @import("../core/style.zig");
const Layout = @import("../core/layout.zig");
const Framebuffer = @import("framebuffer.zig").Framebuffer;
const Color = Style.Color;
const Rect = Layout.Rect;
/// A simple bitmap font
pub const Font = struct {
/// Glyph data (1 bit per pixel)
glyphs: []const u8,
/// Width of each character
char_width: u8,
/// Height of each character
char_height: u8,
/// First character in the font
first_char: u8,
/// Number of characters
num_chars: u16,
const Self = @This();
/// Get the width of a character
pub fn charWidth(self: Self) u8 {
return self.char_width;
}
/// Get the height of a character
pub fn charHeight(self: Self) u8 {
return self.char_height;
}
/// Get the width of a string in pixels
pub fn textWidth(self: Self, text: []const u8) u32 {
return @as(u32, self.char_width) * @as(u32, @intCast(text.len));
}
/// Draw a single character
pub fn drawChar(
self: Self,
fb: *Framebuffer,
x: i32,
y: i32,
char: u8,
color: Color,
clip: Rect,
) void {
// Check if character is in font
if (char < self.first_char) return;
const idx = char - self.first_char;
if (idx >= self.num_chars) return;
// Calculate glyph data offset
const bytes_per_row = (self.char_width + 7) / 8;
const bytes_per_char = @as(usize, bytes_per_row) * @as(usize, self.char_height);
const glyph_offset = @as(usize, idx) * bytes_per_char;
if (glyph_offset + bytes_per_char > self.glyphs.len) return;
const glyph = self.glyphs[glyph_offset..][0..bytes_per_char];
// Draw the glyph
var py: u8 = 0;
while (py < self.char_height) : (py += 1) {
const screen_y = y + @as(i32, py);
if (screen_y < clip.top() or screen_y >= clip.bottom()) continue;
var px: u8 = 0;
while (px < self.char_width) : (px += 1) {
const screen_x = x + @as(i32, px);
if (screen_x < clip.left() or screen_x >= clip.right()) continue;
// Get pixel from glyph data
const row_offset = @as(usize, py) * bytes_per_row;
const byte_idx = row_offset + @as(usize, px / 8);
const bit_idx: u3 = @intCast(7 - (px % 8));
if (byte_idx < glyph.len) {
const pixel_set = (glyph[byte_idx] >> bit_idx) & 1;
if (pixel_set != 0) {
fb.setPixel(screen_x, screen_y, color);
}
}
}
}
}
/// Draw a string
pub fn drawText(
self: Self,
fb: *Framebuffer,
x: i32,
y: i32,
text: []const u8,
color: Color,
clip: Rect,
) void {
var cx = x;
for (text) |char| {
if (char == '\n') {
// Handle newline - not implemented in simple version
continue;
}
self.drawChar(fb, cx, y, char, color, clip);
cx += @as(i32, self.char_width);
}
}
};
// =============================================================================
// Built-in 8x8 font
// =============================================================================
/// Simple 8x8 bitmap font (ASCII 32-126)
pub const default_font = Font{
.glyphs = &default_font_data,
.char_width = 8,
.char_height = 8,
.first_char = 32,
.num_chars = 95,
};
// 8x8 font data for ASCII 32-126
// Each character is 8 bytes (8 rows, 1 byte per row)
const default_font_data = blk: {
// This is a minimal font - in production, use a proper font
var data: [95 * 8]u8 = undefined;
// Initialize all to zero
for (&data) |*b| {
b.* = 0;
}
// Space (32)
// Already zero
// ! (33)
const exclaim = [8]u8{ 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x00 };
@memcpy(data[1 * 8 ..][0..8], &exclaim);
// A (65)
const a_upper = [8]u8{ 0x3C, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x00 };
@memcpy(data[33 * 8 ..][0..8], &a_upper);
// B (66)
const b_upper = [8]u8{ 0x7C, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x7C, 0x00 };
@memcpy(data[34 * 8 ..][0..8], &b_upper);
// C (67)
const c_upper = [8]u8{ 0x3C, 0x66, 0x60, 0x60, 0x60, 0x66, 0x3C, 0x00 };
@memcpy(data[35 * 8 ..][0..8], &c_upper);
// ... Add more characters as needed
// a (97)
const a_lower = [8]u8{ 0x00, 0x00, 0x3C, 0x06, 0x3E, 0x66, 0x3E, 0x00 };
@memcpy(data[65 * 8 ..][0..8], &a_lower);
// b (98)
const b_lower = [8]u8{ 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x7C, 0x00 };
@memcpy(data[66 * 8 ..][0..8], &b_lower);
// 0 (48)
const zero = [8]u8{ 0x3C, 0x66, 0x6E, 0x76, 0x66, 0x66, 0x3C, 0x00 };
@memcpy(data[16 * 8 ..][0..8], &zero);
// 1 (49)
const one = [8]u8{ 0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00 };
@memcpy(data[17 * 8 ..][0..8], &one);
break :blk data;
};
// =============================================================================
// Tests
// =============================================================================
test "Font basic" {
const font = default_font;
try std.testing.expectEqual(@as(u8, 8), font.charWidth());
try std.testing.expectEqual(@as(u8, 8), font.charHeight());
try std.testing.expectEqual(@as(u32, 40), font.textWidth("Hello"));
}
test "Font draw" {
var fb = try Framebuffer.init(std.testing.allocator, 100, 100);
defer fb.deinit();
const font = default_font;
const clip = Rect.init(0, 0, 100, 100);
fb.clear(Color.black);
font.drawChar(&fb, 10, 10, 'A', Color.white, clip);
// Just verify it doesn't crash
}

234
src/render/framebuffer.zig Normal file
View file

@ -0,0 +1,234 @@
//! Framebuffer - Pixel buffer for software rendering
//!
//! A simple 2D array of RGBA pixels.
//! The software rasterizer writes to this, then it's blitted to the screen.
const std = @import("std");
const Allocator = std.mem.Allocator;
const Style = @import("../core/style.zig");
const Color = Style.Color;
/// A 2D pixel buffer
pub const Framebuffer = struct {
allocator: Allocator,
pixels: []u32,
width: u32,
height: u32,
const Self = @This();
/// Create a new framebuffer
pub fn init(allocator: Allocator, width: u32, height: u32) !Self {
const size = @as(usize, width) * @as(usize, height);
const pixels = try allocator.alloc(u32, size);
@memset(pixels, 0);
return .{
.allocator = allocator,
.pixels = pixels,
.width = width,
.height = height,
};
}
/// Free the framebuffer
pub fn deinit(self: Self) void {
self.allocator.free(self.pixels);
}
/// Resize the framebuffer
pub fn resize(self: *Self, width: u32, height: u32) !void {
if (width == self.width and height == self.height) return;
const size = @as(usize, width) * @as(usize, height);
const new_pixels = try self.allocator.alloc(u32, size);
@memset(new_pixels, 0);
self.allocator.free(self.pixels);
self.pixels = new_pixels;
self.width = width;
self.height = height;
}
/// Clear the entire buffer to a color
pub fn clear(self: *Self, color: Color) void {
const c = color.toABGR();
@memset(self.pixels, c);
}
/// Get pixel at (x, y)
pub fn getPixel(self: Self, x: i32, y: i32) ?u32 {
if (x < 0 or y < 0) return null;
const ux = @as(u32, @intCast(x));
const uy = @as(u32, @intCast(y));
if (ux >= self.width or uy >= self.height) return null;
const idx = uy * self.width + ux;
return self.pixels[idx];
}
/// Set pixel at (x, y)
pub fn setPixel(self: *Self, x: i32, y: i32, color: Color) void {
if (x < 0 or y < 0) return;
const ux = @as(u32, @intCast(x));
const uy = @as(u32, @intCast(y));
if (ux >= self.width or uy >= self.height) return;
const idx = uy * self.width + ux;
if (color.a == 255) {
self.pixels[idx] = color.toABGR();
} else if (color.a > 0) {
// Blend with existing pixel
const existing = self.pixels[idx];
const bg = Color{
.r = @truncate(existing),
.g = @truncate(existing >> 8),
.b = @truncate(existing >> 16),
.a = @truncate(existing >> 24),
};
self.pixels[idx] = color.blend(bg).toABGR();
}
}
/// Draw a filled rectangle
pub fn fillRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color) void {
const x_start = @max(0, x);
const y_start = @max(0, y);
const x_end = @min(@as(i32, @intCast(self.width)), x + @as(i32, @intCast(w)));
const y_end = @min(@as(i32, @intCast(self.height)), y + @as(i32, @intCast(h)));
if (x_start >= x_end or y_start >= y_end) return;
const c = color.toABGR();
var py = y_start;
while (py < y_end) : (py += 1) {
const row_start = @as(u32, @intCast(py)) * self.width;
var px = x_start;
while (px < x_end) : (px += 1) {
const idx = row_start + @as(u32, @intCast(px));
if (color.a == 255) {
self.pixels[idx] = c;
} else if (color.a > 0) {
const existing = self.pixels[idx];
const bg = Color{
.r = @truncate(existing),
.g = @truncate(existing >> 8),
.b = @truncate(existing >> 16),
.a = @truncate(existing >> 24),
};
self.pixels[idx] = color.blend(bg).toABGR();
}
}
}
}
/// Draw a rectangle outline
pub fn drawRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Color) void {
if (w == 0 or h == 0) return;
// Top and bottom
self.fillRect(x, y, w, 1, color);
self.fillRect(x, y + @as(i32, @intCast(h)) - 1, w, 1, color);
// Left and right
self.fillRect(x, y + 1, 1, h -| 2, color);
self.fillRect(x + @as(i32, @intCast(w)) - 1, y + 1, 1, h -| 2, color);
}
/// Draw a horizontal line
pub fn drawHLine(self: *Self, x: i32, y: i32, w: u32, color: Color) void {
self.fillRect(x, y, w, 1, color);
}
/// Draw a vertical line
pub fn drawVLine(self: *Self, x: i32, y: i32, h: u32, color: Color) void {
self.fillRect(x, y, 1, h, color);
}
/// Draw a line (Bresenham's algorithm)
pub fn drawLine(self: *Self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) void {
var x = x0;
var y = y0;
const dx = @abs(x1 - x0);
const dy = @abs(y1 - y0);
const sx: i32 = if (x0 < x1) 1 else -1;
const sy: i32 = if (y0 < y1) 1 else -1;
var err = @as(i32, @intCast(dx)) - @as(i32, @intCast(dy));
while (true) {
self.setPixel(x, y, color);
if (x == x1 and y == y1) break;
const e2 = err * 2;
if (e2 > -@as(i32, @intCast(dy))) {
err -= @as(i32, @intCast(dy));
x += sx;
}
if (e2 < @as(i32, @intCast(dx))) {
err += @as(i32, @intCast(dx));
y += sy;
}
}
}
/// Get raw pixel data (for blitting to SDL texture)
pub fn getData(self: Self) []const u32 {
return self.pixels;
}
/// Get pitch in bytes
pub fn getPitch(self: Self) u32 {
return self.width * 4;
}
};
// =============================================================================
// Tests
// =============================================================================
test "Framebuffer basic" {
var fb = try Framebuffer.init(std.testing.allocator, 100, 100);
defer fb.deinit();
fb.clear(Color.black);
fb.setPixel(50, 50, Color.white);
const pixel = fb.getPixel(50, 50);
try std.testing.expect(pixel != null);
}
test "Framebuffer fillRect" {
var fb = try Framebuffer.init(std.testing.allocator, 100, 100);
defer fb.deinit();
fb.clear(Color.black);
fb.fillRect(10, 10, 20, 20, Color.red);
// Check inside
const inside = fb.getPixel(15, 15);
try std.testing.expect(inside != null);
try std.testing.expectEqual(Color.red.toABGR(), inside.?);
// Check outside
const outside = fb.getPixel(5, 5);
try std.testing.expect(outside != null);
try std.testing.expectEqual(Color.black.toABGR(), outside.?);
}
test "Framebuffer out of bounds" {
var fb = try Framebuffer.init(std.testing.allocator, 100, 100);
defer fb.deinit();
// These should not crash
fb.setPixel(-1, 50, Color.white);
fb.setPixel(50, -1, Color.white);
fb.setPixel(100, 50, Color.white);
fb.setPixel(50, 100, Color.white);
try std.testing.expect(fb.getPixel(-1, 50) == null);
}

222
src/render/software.zig Normal file
View file

@ -0,0 +1,222 @@
//! SoftwareRenderer - Executes draw commands on a framebuffer
//!
//! This is the core of our rendering system.
//! It takes DrawCommands and turns them into pixels.
const std = @import("std");
const Command = @import("../core/command.zig");
const Style = @import("../core/style.zig");
const Layout = @import("../core/layout.zig");
const Framebuffer = @import("framebuffer.zig").Framebuffer;
const Font = @import("font.zig").Font;
const Color = Style.Color;
const Rect = Layout.Rect;
const DrawCommand = Command.DrawCommand;
/// Software renderer state
pub const SoftwareRenderer = struct {
framebuffer: *Framebuffer,
default_font: ?*Font,
/// Clipping stack
clip_stack: [16]Rect,
clip_depth: usize,
const Self = @This();
/// Initialize the renderer
pub fn init(framebuffer: *Framebuffer) Self {
return .{
.framebuffer = framebuffer,
.default_font = null,
.clip_stack = undefined,
.clip_depth = 0,
};
}
/// Set the default font
pub fn setDefaultFont(self: *Self, font: *Font) void {
self.default_font = font;
}
/// Get the current clip rectangle
pub fn getClip(self: Self) Rect {
if (self.clip_depth == 0) {
return Rect.init(0, 0, self.framebuffer.width, self.framebuffer.height);
}
return self.clip_stack[self.clip_depth - 1];
}
/// Execute a single draw command
pub fn execute(self: *Self, cmd: DrawCommand) void {
switch (cmd) {
.rect => |r| self.drawRect(r),
.text => |t| self.drawText(t),
.line => |l| self.drawLine(l),
.rect_outline => |r| self.drawRectOutline(r),
.clip => |c| self.pushClip(c),
.clip_end => self.popClip(),
.nop => {},
}
}
/// Execute all commands in a list
pub fn executeAll(self: *Self, commands: []const DrawCommand) void {
for (commands) |cmd| {
self.execute(cmd);
}
}
/// Clear the framebuffer
pub fn clear(self: *Self, color: Color) void {
self.framebuffer.clear(color);
}
// =========================================================================
// Private drawing functions
// =========================================================================
fn drawRect(self: *Self, r: Command.RectCommand) void {
const clip = self.getClip();
// Clip the rectangle
const clipped = Rect.init(r.x, r.y, r.w, r.h).intersection(clip);
if (clipped.isEmpty()) return;
self.framebuffer.fillRect(
clipped.x,
clipped.y,
clipped.w,
clipped.h,
r.color,
);
}
fn drawText(self: *Self, t: Command.TextCommand) void {
const font = if (t.font) |f|
@as(*Font, @ptrCast(@alignCast(f)))
else
self.default_font orelse return;
const clip = self.getClip();
// Simple text rendering - character by character
var x = t.x;
for (t.text) |char| {
if (char == '\n') {
// TODO: Handle newlines
continue;
}
// Check if character is visible
if (x >= clip.right()) break;
// Render character
font.drawChar(self.framebuffer, x, t.y, char, t.color, clip);
x += @as(i32, @intCast(font.charWidth()));
}
}
fn drawLine(self: *Self, l: Command.LineCommand) void {
// TODO: Clip line to clip rectangle
self.framebuffer.drawLine(l.x1, l.y1, l.x2, l.y2, l.color);
}
fn drawRectOutline(self: *Self, r: Command.RectOutlineCommand) void {
const clip = self.getClip();
// Draw each edge as a filled rect
// Top
const top_clipped = Rect.init(r.x, r.y, r.w, r.thickness).intersection(clip);
if (!top_clipped.isEmpty()) {
self.framebuffer.fillRect(top_clipped.x, top_clipped.y, top_clipped.w, top_clipped.h, r.color);
}
// Bottom
const bottom_y = r.y + @as(i32, @intCast(r.h)) - @as(i32, @intCast(r.thickness));
const bottom_clipped = Rect.init(r.x, bottom_y, r.w, r.thickness).intersection(clip);
if (!bottom_clipped.isEmpty()) {
self.framebuffer.fillRect(bottom_clipped.x, bottom_clipped.y, bottom_clipped.w, bottom_clipped.h, r.color);
}
// Left
const inner_y = r.y + @as(i32, @intCast(r.thickness));
const inner_h = r.h -| (r.thickness * 2);
const left_clipped = Rect.init(r.x, inner_y, r.thickness, inner_h).intersection(clip);
if (!left_clipped.isEmpty()) {
self.framebuffer.fillRect(left_clipped.x, left_clipped.y, left_clipped.w, left_clipped.h, r.color);
}
// Right
const right_x = r.x + @as(i32, @intCast(r.w)) - @as(i32, @intCast(r.thickness));
const right_clipped = Rect.init(right_x, inner_y, r.thickness, inner_h).intersection(clip);
if (!right_clipped.isEmpty()) {
self.framebuffer.fillRect(right_clipped.x, right_clipped.y, right_clipped.w, right_clipped.h, r.color);
}
}
fn pushClip(self: *Self, c: Command.ClipCommand) void {
if (self.clip_depth >= self.clip_stack.len) return;
const new_clip = Rect.init(c.x, c.y, c.w, c.h);
const current = self.getClip();
const clipped = new_clip.intersection(current);
self.clip_stack[self.clip_depth] = clipped;
self.clip_depth += 1;
}
fn popClip(self: *Self) void {
if (self.clip_depth > 0) {
self.clip_depth -= 1;
}
}
};
// =============================================================================
// Tests
// =============================================================================
test "SoftwareRenderer basic" {
var fb = try Framebuffer.init(std.testing.allocator, 100, 100);
defer fb.deinit();
var renderer = SoftwareRenderer.init(&fb);
renderer.clear(Color.black);
renderer.execute(Command.rect(10, 10, 20, 20, Color.red));
const pixel = fb.getPixel(15, 15);
try std.testing.expect(pixel != null);
try std.testing.expectEqual(Color.red.toABGR(), pixel.?);
}
test "SoftwareRenderer clipping" {
var fb = try Framebuffer.init(std.testing.allocator, 100, 100);
defer fb.deinit();
var renderer = SoftwareRenderer.init(&fb);
renderer.clear(Color.black);
// Set clip to 50x50
renderer.execute(Command.clip(0, 0, 50, 50));
// Draw rect that extends beyond clip
renderer.execute(Command.rect(40, 40, 30, 30, Color.red));
renderer.execute(Command.clipEnd());
// Check inside clip (should be red)
const inside = fb.getPixel(45, 45);
try std.testing.expect(inside != null);
try std.testing.expectEqual(Color.red.toABGR(), inside.?);
// Check outside clip (should be black)
const outside = fb.getPixel(55, 55);
try std.testing.expect(outside != null);
try std.testing.expectEqual(Color.black.toABGR(), outside.?);
}

93
src/zcatgui.zig Normal file
View file

@ -0,0 +1,93 @@
//! zCatGui - Immediate Mode GUI Library for Zig
//!
//! A software-rendered, cross-platform GUI library with macro recording support.
//!
//! ## Features
//! - Immediate mode paradigm (no callbacks, explicit state)
//! - Software rendering (works everywhere, including SSH)
//! - Macro system for recording/replaying user actions
//! - SDL2 backend for cross-platform support
//!
//! ## Quick Start
//! ```zig
//! const zcatgui = @import("zcatgui");
//!
//! pub fn main() !void {
//! var app = try zcatgui.App.init(allocator, "My App", 800, 600);
//! defer app.deinit();
//!
//! while (app.running) {
//! app.beginFrame();
//!
//! if (app.button("Click me!")) {
//! // Handle click
//! }
//!
//! app.endFrame();
//! }
//! }
//! ```
const std = @import("std");
// =============================================================================
// Core modules
// =============================================================================
pub const Context = @import("core/context.zig").Context;
pub const Layout = @import("core/layout.zig");
pub const Style = @import("core/style.zig");
pub const Input = @import("core/input.zig");
pub const Command = @import("core/command.zig");
// =============================================================================
// Macro system
// =============================================================================
pub const macro = @import("macro/macro.zig");
pub const MacroRecorder = macro.MacroRecorder;
pub const MacroPlayer = macro.MacroPlayer;
pub const KeyEvent = macro.KeyEvent;
// =============================================================================
// Rendering
// =============================================================================
pub const render = struct {
pub const Framebuffer = @import("render/framebuffer.zig").Framebuffer;
pub const SoftwareRenderer = @import("render/software.zig").SoftwareRenderer;
pub const Font = @import("render/font.zig").Font;
};
// =============================================================================
// Backend
// =============================================================================
pub const backend = struct {
pub const Backend = @import("backend/backend.zig").Backend;
pub const Sdl2Backend = @import("backend/sdl2.zig").Sdl2Backend;
};
// =============================================================================
// Widgets (to be implemented)
// =============================================================================
pub const widgets = struct {
// pub const Button = @import("widgets/button.zig").Button;
// pub const Label = @import("widgets/label.zig").Label;
// pub const Input = @import("widgets/input.zig").Input;
// pub const Select = @import("widgets/select.zig").Select;
// pub const Table = @import("widgets/table.zig").Table;
// pub const Panel = @import("widgets/panel.zig").Panel;
// pub const Split = @import("widgets/split.zig").Split;
// pub const Modal = @import("widgets/modal.zig").Modal;
};
// =============================================================================
// Re-exports for convenience
// =============================================================================
pub const Color = Style.Color;
pub const Rect = Layout.Rect;
pub const Constraint = Layout.Constraint;
// =============================================================================
// Tests
// =============================================================================
test {
std.testing.refAllDecls(@This());
}