zcatgui/docs/KEYBOARD_REFACTORING_PLAN.md
reugenio 7073ccef9f docs: Plan de refactoring sistema de teclado
Auditoría comparando con Gio, microui y DVUI identificó 4 problemas:
1. SDL_KEYDOWN y SDL_TEXTINPUT separados
2. Dos arrays para eventos de teclado
3. Sin timing en Context para animaciones
4. Shortcuts duplicados en cada widget

Plan de 4 fases aprobado (~7-11 horas total).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 22:43:05 +01:00

259 lines
7.4 KiB
Markdown

# Plan de Refactoring: Sistema de Eventos de Teclado
> **Fecha:** 2025-12-11
> **Estado:** APROBADO - Pendiente implementación
> **Prioridad:** ALTA
---
## Contexto
Auditoría realizada comparando zcatgui con librerías de referencia (Gio, microui, DVUI) identificó 4 problemas de duplicación/inconsistencia en el manejo de eventos de teclado.
## Problemas Identificados
| # | Problema | Impacto |
|---|----------|---------|
| 1 | SDL_KEYDOWN y SDL_TEXTINPUT son eventos separados | `KeyEvent.char` siempre es `null`, texto llega en array aparte |
| 2 | Dos arrays: `key_events[]` + `text_input[]` | Widgets deben leer ambos para funcionar |
| 3 | Sin timing en Context | Imposible implementar cursor parpadeante |
| 4 | Shortcuts duplicados en cada widget | Ctrl+A, Ctrl+Z reimplementados en text_input, table, etc. |
## Comparación con Referencias
| Aspecto | zcatgui | Gio | microui | DVUI |
|---------|---------|-----|---------|------|
| Evento único para teclado | NO | SI | SI | SI |
| Carácter en el evento | NO | SI | SI | SI |
| Timing disponible | NO | SI | SI | SI |
| Shortcuts centralizados | NO | SI | Parcial | Parcial |
| Cursor parpadeante | NO | SI | SI | SI |
## Plan de Implementación
### Fase 1: Timing en Context (2-3 horas)
**Objetivo:** Permitir animaciones y cursor parpadeante.
**Cambios:**
```zig
// context.zig
pub const Context = struct {
// ... campos existentes ...
// NUEVO: Timing para animaciones
current_time_ms: u64 = 0,
frame_delta_ms: u32 = 0,
pub fn setFrameTime(self: *Self, time_ms: u64) void {
self.frame_delta_ms = @intCast(time_ms - self.current_time_ms);
self.current_time_ms = time_ms;
}
};
```
**Uso en aplicación (main.zig):**
```zig
while (running) {
ctx.beginFrame();
ctx.setFrameTime(@intCast(std.time.milliTimestamp()));
// ... resto del loop
}
```
**Archivos a modificar:**
- `src/core/context.zig` - Agregar campos y método
- `zsimifactu/src/main.zig` - Llamar setFrameTime()
---
### Fase 2: Sistema de Shortcuts (2-3 horas)
**Objetivo:** Centralizar manejo de Ctrl+, Alt+, etc.
**Nuevo archivo `src/core/shortcuts.zig`:**
```zig
pub const Shortcut = struct {
name: []const u8,
key: Key,
modifiers: KeyModifiers,
};
pub const ShortcutManager = struct {
shortcuts: std.ArrayList(Shortcut),
pub fn register(self: *Self, name: []const u8, key: Key, mods: KeyModifiers) void;
pub fn isActive(self: Self, name: []const u8, input: *const InputState) bool;
pub fn getActive(self: Self, input: *const InputState) ?[]const u8;
};
// Shortcuts predefinidos
pub const BUILTIN_SHORTCUTS = [_]Shortcut{
.{ .name = "select_all", .key = .a, .modifiers = .{ .ctrl = true } },
.{ .name = "copy", .key = .c, .modifiers = .{ .ctrl = true } },
.{ .name = "paste", .key = .v, .modifiers = .{ .ctrl = true } },
.{ .name = "cut", .key = .x, .modifiers = .{ .ctrl = true } },
.{ .name = "undo", .key = .z, .modifiers = .{ .ctrl = true } },
.{ .name = "redo", .key = .z, .modifiers = .{ .ctrl = true, .shift = true } },
.{ .name = "save", .key = .s, .modifiers = .{ .ctrl = true } },
};
```
**Uso en widgets:**
```zig
// Antes (duplicado en cada widget):
if (key_event.key == .a and key_event.modifiers.ctrl) {
state.selectAll();
}
// Después (centralizado):
if (ctx.shortcuts.isActive("select_all", &ctx.input)) {
state.selectAll();
}
```
**Archivos a modificar:**
- `src/core/shortcuts.zig` - NUEVO
- `src/core/context.zig` - Agregar ShortcutManager
- `src/widgets/text_input.zig` - Usar shortcuts
- `src/widgets/table.zig` - Usar shortcuts
- `src/zcatgui.zig` - Re-exportar
---
### Fase 3: Búsqueda Incremental en Tabla (2-3 horas)
**Objetivo:** Poder escribir letras para buscar en tabla/lista.
**Cambios en TableState:**
```zig
pub const TableState = struct {
// ... campos existentes ...
// NUEVO: Búsqueda incremental
search_buffer: [64]u8 = [_]u8{0} ** 64,
search_len: usize = 0,
search_last_time: u64 = 0,
search_timeout_ms: u64 = 1000, // Reset después de 1s sin teclear
pub fn handleSearchChar(self: *Self, char: u8, current_time: u64) void {
// Reset si pasó timeout
if (current_time - self.search_last_time > self.search_timeout_ms) {
self.search_len = 0;
}
// Agregar carácter
if (self.search_len < self.search_buffer.len) {
self.search_buffer[self.search_len] = char;
self.search_len += 1;
}
self.search_last_time = current_time;
}
pub fn getSearchTerm(self: Self) []const u8 {
return self.search_buffer[0..self.search_len];
}
};
```
**En handleKeyboard():**
```zig
// Procesar caracteres imprimibles para búsqueda
const text_input = ctx.input.getTextInput();
for (text_input) |char| {
if (char >= 32 and char < 127) {
state.handleSearchChar(char, ctx.current_time_ms);
// Buscar primera fila que coincida
// ...
}
}
```
**Archivos a modificar:**
- `src/widgets/table.zig` - Agregar búsqueda
- `src/widgets/list.zig` - Agregar búsqueda (opcional)
- `src/widgets/select.zig` - Agregar búsqueda (opcional)
---
### Fase 4: Cursor Parpadeante (1-2 horas)
**Objetivo:** UX profesional en campos de texto.
**Cambios en text_input.zig:**
```zig
// En drawCursor():
fn drawCursor(ctx: *Context, state: *TextInputState, x: i32, y: i32, h: u32) void {
// Solo mostrar cursor si tiene focus
if (!state.focused) return;
// Parpadeo cada 500ms
const blink_period_ms: u64 = 500;
const cursor_visible = (ctx.current_time_ms / blink_period_ms) % 2 == 0;
if (cursor_visible) {
ctx.pushCommand(.{ .line = .{
.x1 = x,
.y1 = y,
.x2 = x,
.y2 = y + @intCast(h),
.color = theme.cursor_color,
}});
}
}
```
**Consideración CPU:**
- El parpadeo requiere redraw cada 500ms cuando hay focus
- Solución: En idle, solo despertar para parpadeo si TextInput tiene focus
- Alternativa: Cursor sólido (bloque invertido) sin parpadeo
**Archivos a modificar:**
- `src/widgets/text_input.zig` - Cursor parpadea
---
## Orden de Implementación
1. **Fase 1 primero** - Es prerequisito para Fase 3 y 4
2. **Fase 4 segundo** - Beneficio inmediato visible
3. **Fase 2 tercero** - Limpieza de código
4. **Fase 3 último** - Feature nuevo
## Estimación Total
| Fase | Horas | Complejidad |
|------|-------|-------------|
| 1 | 2-3 | Baja |
| 2 | 2-3 | Media |
| 3 | 2-3 | Media |
| 4 | 1-2 | Baja |
| **Total** | **7-11** | - |
## Riesgos
1. **Fase 1 (timing):** Puede afectar idle de CPU si no se maneja bien
2. **Fase 3 (búsqueda):** Requiere que aplicación provea función de matching
3. **Fase 4 (parpadeo):** Conflicto con modelo "redraw on demand"
## Criterios de Éxito
- [ ] Cursor parpadea en TextInput
- [ ] Ctrl+A/C/V/X/Z funcionan igual en todos los widgets
- [ ] Tabla permite buscar escribiendo letras
- [ ] CPU idle sigue siendo ~0% cuando no hay actividad
- [ ] Tests pasan
---
## Referencias
- Auditoría completa: Conversación Claude 2025-12-11
- Gio input: https://gioui.org/doc/learn/events
- microui: https://github.com/rxi/microui
- DVUI: https://github.com/david-vanderson/dvui