# 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