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:
commit
59c597fc18
22 changed files with 5752 additions and 0 deletions
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal 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
535
CLAUDE.md
Normal 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
82
build.zig
Normal 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
18
build.zig.zon
Normal 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
529
docs/ARCHITECTURE.md
Normal 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/)
|
||||
431
docs/research/GIO_UI_ANALYSIS.md
Normal file
431
docs/research/GIO_UI_ANALYSIS.md
Normal 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)
|
||||
502
docs/research/IMMEDIATE_MODE_LIBS.md
Normal file
502
docs/research/IMMEDIATE_MODE_LIBS.md
Normal 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)
|
||||
617
docs/research/SIMIFACTU_FYNE_ANALYSIS.md
Normal file
617
docs/research/SIMIFACTU_FYNE_ANALYSIS.md
Normal 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
81
examples/hello.zig
Normal 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
176
examples/macro_demo.zig
Normal 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
78
src/backend/backend.zig
Normal 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
323
src/backend/sdl2.zig
Normal 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
159
src/core/command.zig
Normal 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
161
src/core/context.zig
Normal 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
299
src/core/input.zig
Normal 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
408
src/core/layout.zig
Normal 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
244
src/core/style.zig
Normal 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
340
src/macro/macro.zig
Normal 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
207
src/render/font.zig
Normal 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
234
src/render/framebuffer.zig
Normal 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
222
src/render/software.zig
Normal 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
93
src/zcatgui.zig
Normal 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());
|
||||
}
|
||||
Loading…
Reference in a new issue