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>
259 lines
7.4 KiB
Markdown
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
|