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