From 7073ccef9f2bf6e553d4b23aa54df38239b3c9ff Mon Sep 17 00:00:00 2001 From: reugenio Date: Thu, 11 Dec 2025 22:43:05 +0100 Subject: [PATCH] docs: Plan de refactoring sistema de teclado MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/KEYBOARD_REFACTORING_PLAN.md | 259 ++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 docs/KEYBOARD_REFACTORING_PLAN.md diff --git a/docs/KEYBOARD_REFACTORING_PLAN.md b/docs/KEYBOARD_REFACTORING_PLAN.md new file mode 100644 index 0000000..86d9a14 --- /dev/null +++ b/docs/KEYBOARD_REFACTORING_PLAN.md @@ -0,0 +1,259 @@ +# 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