# Rediseño del Sistema de Focus en zcatgui > **Fecha**: 2025-12-12 > **Estado**: EN IMPLEMENTACIÓN > **Consensuado**: Sí --- ## 1. PROBLEMA DETECTADO ### 1.1 Síntomas observados en zsimifactu 1. **Navegación errática con flechas**: Pulsar flecha abajo mueve la selección una vez, la segunda vez no hace nada 2. **Tab afecta widgets equivocados**: Al pulsar Tab en el panel derecho, eventualmente mueve la selección del panel izquierdo 3. **Focus visual incorrecto**: Al hacer clic en un campo del panel derecho, ambos paneles muestran indicador de focus 4. **Tab requiere múltiples pulsaciones**: Hay que pulsar Tab 4 veces para que haga efecto ### 1.2 Causa raíz zcatgui tenía **dos sistemas de focus paralelos** que no se comunicaban: 1. **`FocusManager`** en `widgets/focus.zig`: - IDs de tipo `u32` - No integrado con Context - Usado parcialmente por algunos widgets 2. **`FocusGroupManager`** en `core/focus_group.zig`: - IDs de tipo `u64` - No usado por widgets - Diseñado pero no implementado completamente Esta duplicidad causaba: - Widgets registrándose en sistemas diferentes - Tab procesado inconsistentemente - Estado de focus desincronizado --- ## 2. ANÁLISIS DE REFERENCIAS ### 2.1 microui (C) - Referencia principal ```c // UN solo campo de focus en el contexto struct mu_Context { mu_Id hover; // Widget bajo el cursor mu_Id focus; // Widget con focus de teclado mu_Id last_id; // Último ID procesado // ... }; // Generación de ID con hash FNV-1a mu_Id mu_get_id(mu_Context *ctx, const void *data, int size); // Flujo: // 1. Widget llama mu_update_control(ctx, id, rect) // 2. Si mouse_over && click → ctx->focus = id // 3. Widget consulta: if (ctx->focus == id) { procesar teclado } ``` **Características clave:** - Focus plano (sin grupos) - Tab NO implementado (aplicación lo maneja) - Simple y funcional ### 2.2 Gio (Go) - Referencia moderna ```go // Focus mediante tags (punteros) type FocusCmd struct { Tag event.Tag // Solicitar focus } type FocusEvent struct { Focus bool // Notificación de cambio } // El Router maneja Tab automáticamente Router.MoveFocus(dir key.FocusDirection) ``` **Características clave:** - Focus plano (sin grupos) - Tab/Shift+Tab manejado por el Router - Widgets se registran con FocusFilter --- ## 3. DISEÑO CONSENSUADO ### 3.1 Principio **"Grupos opcionales con comportamiento por defecto sensato"** - Si la app NO crea grupos → existe un grupo global implícito (grupo 0) - Tab navega todos los widgets registrados (como microui/Gio) - Si la app crea grupos → Tab navega dentro del grupo activo - API de aplicación para cambiar grupo activo (ej: F6) ### 3.2 Arquitectura ``` ┌─────────────────────────────────────────────────────────────┐ │ Context │ ├─────────────────────────────────────────────────────────────┤ │ focus: FocusSystem │ │ ├── focused_id: ?u64 (widget con focus actual) │ │ ├── active_group: u64 (grupo activo, default=0) │ │ ├── groups: [MAX_GROUPS]Group (array de grupos) │ │ │ └── Group │ │ │ ├── id: u64 │ │ │ ├── widgets: [MAX]u64 (IDs registrados) │ │ │ └── count: usize │ │ └── hover_id: ?u64 (widget bajo cursor) │ └─────────────────────────────────────────────────────────────┘ ``` ### 3.3 Flujo de operación ``` INICIO DE FRAME (beginFrame): 1. Limpiar registros de widgets de todos los grupos 2. Mantener focused_id y active_group DURANTE EL FRAME (widgets dibujándose): 1. Widget genera ID: @intFromPtr(&state) o hash 2. Widget llama ctx.focus.register(id) → Se registra en el grupo activo 3. Widget consulta ctx.focus.hasFocus(id) → true si focused_id == id 4. Si widget recibe clic → ctx.focus.request(id) → focused_id = id PROCESAMIENTO DE TAB: 1. App llama ctx.focus.handleTab(shift) 2. Sistema busca siguiente widget en grupo activo 3. focused_id = siguiente widget FIN DE FRAME (endFrame): 1. Si focused_id no está en ningún grupo → focused_id = null 2. (Limpieza de widgets no re-registrados) ``` ### 3.4 API pública ```zig pub const FocusSystem = struct { // Estado focused_id: ?u64 = null, hover_id: ?u64 = null, active_group: u64 = 0, // Grupos (grupo 0 = global implícito) groups: [MAX_GROUPS]FocusGroup = undefined, group_count: usize = 1, // Siempre existe grupo 0 // ───────────────────────────────────────────── // API para widgets // ───────────────────────────────────────────── /// Registrar widget como focusable (en grupo activo) pub fn register(self: *FocusSystem, widget_id: u64) void; /// ¿Este widget tiene focus? pub fn hasFocus(self: *FocusSystem, widget_id: u64) bool; /// Solicitar focus para este widget pub fn request(self: *FocusSystem, widget_id: u64) void; /// ¿Este widget tiene hover? pub fn hasHover(self: *FocusSystem, widget_id: u64) bool; /// Establecer hover pub fn setHover(self: *FocusSystem, widget_id: u64) void; // ───────────────────────────────────────────── // API para aplicación // ───────────────────────────────────────────── /// Crear un nuevo grupo de focus pub fn createGroup(self: *FocusSystem, group_id: u64) void; /// Activar un grupo (Tab navegará dentro de él) pub fn setActiveGroup(self: *FocusSystem, group_id: u64) void; /// Obtener grupo activo pub fn getActiveGroup(self: *FocusSystem) u64; /// Mover focus al siguiente/anterior widget (Tab/Shift+Tab) pub fn focusNext(self: *FocusSystem) void; pub fn focusPrev(self: *FocusSystem) void; /// Limpiar focus pub fn clearFocus(self: *FocusSystem) void; // ───────────────────────────────────────────── // API interna (llamada por Context) // ───────────────────────────────────────────── /// Llamado al inicio de frame pub fn beginFrame(self: *FocusSystem) void; /// Llamado al final de frame pub fn endFrame(self: *FocusSystem) void; }; ``` --- ## 4. PLAN DE IMPLEMENTACIÓN ### 4.1 Fase 1: Nuevo FocusSystem en core/ 1. Crear `core/focus.zig` con `FocusSystem` 2. Estructura simple y autocontenida 3. Tests unitarios ### 4.2 Fase 2: Integrar en Context 1. Reemplazar campos actuales por `focus: FocusSystem` 2. Context delega a FocusSystem 3. beginFrame/endFrame llaman a focus.beginFrame/endFrame ### 4.3 Fase 3: Adaptar widgets 1. **TextInput**: usar `ctx.focus.register()`, `ctx.focus.hasFocus()`, `ctx.focus.request()` 2. **Table**: igual 3. **Otros widgets focusables**: igual ### 4.4 Fase 4: Eliminar código obsoleto 1. Eliminar `widgets/focus.zig` (FocusManager viejo) 2. Eliminar `core/focus_group.zig` (reemplazado por nuevo sistema) 3. Limpiar imports en `widgets.zig` y `zcatgui.zig` ### 4.5 Fase 5: Adaptar zsimifactu 1. Crear grupos de focus para cada panel 2. Activar grupo antes de dibujar cada panel 3. Manejar F6 para cambiar entre grupos --- ## 5. DETALLES DE IMPLEMENTACIÓN ### 5.1 Generación de IDs Los widgets generan IDs únicos usando la dirección de memoria de su estado: ```zig // En TextInput const widget_id: u64 = @intFromPtr(state); // En Table const widget_id: u64 = @intFromPtr(state); ``` Esto garantiza unicidad sin necesidad de strings o hashes. ### 5.2 Grupo global implícito El grupo 0 siempre existe y es el default: ```zig pub fn init() FocusSystem { var self = FocusSystem{}; // Grupo 0 siempre existe self.groups[0] = FocusGroup.init(0); self.group_count = 1; return self; } ``` ### 5.3 Navegación Tab ```zig pub fn focusNext(self: *FocusSystem) void { const group = &self.groups[self.findGroupIndex(self.active_group)]; if (group.count == 0) return; // Encontrar índice actual const current_idx = if (self.focused_id) |fid| group.indexOf(fid) orelse 0 else group.count - 1; // Para que next sea 0 // Siguiente (circular) const next_idx = (current_idx + 1) % group.count; self.focused_id = group.widgets[next_idx]; } ``` ### 5.4 Limpieza en endFrame Los widgets que no se re-registran en un frame se consideran "desaparecidos". Si el widget con focus desaparece, se limpia el focus: ```zig pub fn endFrame(self: *FocusSystem) void { // Si el widget con focus no está registrado en ningún grupo, limpiar if (self.focused_id) |fid| { var found = false; for (self.groups[0..self.group_count]) |group| { if (group.contains(fid)) { found = true; break; } } if (!found) { self.focused_id = null; } } } ``` --- ## 6. MIGRACIÓN ### 6.1 Cambios en Context Antes: ```zig pub const Context = struct { focus: FocusManager, // Viejo focus_groups: FocusGroupManager, // Viejo // ... }; ``` Después: ```zig pub const Context = struct { focus: FocusSystem, // Nuevo, unificado // ... }; ``` ### 6.2 Cambios en widgets Antes: ```zig // TextInput const widget_id = ctx.getId(state.buffer.ptr[0..1]); ctx.focus.setFocus(widget_id); const has_focus = ctx.focus.focused_id == widget_id; ``` Después: ```zig // TextInput const widget_id: u64 = @intFromPtr(state); ctx.focus.register(widget_id); if (clicked) ctx.focus.request(widget_id); const has_focus = ctx.focus.hasFocus(widget_id); ``` ### 6.3 Cambios en aplicación (zsimifactu) ```zig // Crear grupos al inicio ctx.focus.createGroup(1); // Panel lista ctx.focus.createGroup(2); // Panel detalle // En el render loop, antes de cada panel: ctx.focus.setActiveGroup(1); who_list_panel.draw(...); ctx.focus.setActiveGroup(2); who_detail_panel.draw(...); // F6 para cambiar panel if (key == .f6) { const next_group = if (ctx.focus.getActiveGroup() == 1) 2 else 1; ctx.focus.setActiveGroup(next_group); } // Tab manejado automáticamente por Context ``` --- ## 7. TESTING ### 7.1 Tests unitarios (core/focus.zig) - [ ] FocusSystem.init() crea grupo 0 - [ ] register() añade widget al grupo activo - [ ] hasFocus() retorna true/false correctamente - [ ] request() cambia focused_id - [ ] focusNext() navega circularmente - [ ] focusPrev() navega circularmente inverso - [ ] createGroup() añade grupos - [ ] setActiveGroup() cambia grupo activo - [ ] endFrame() limpia focus de widgets no registrados ### 7.2 Tests de integración (zsimifactu) - [ ] Tab navega entre campos del panel activo - [ ] Tab NO afecta widgets de otros paneles - [ ] F6 cambia entre paneles - [ ] Clic en widget cambia focus a ese widget - [ ] Un solo indicador visual de focus a la vez --- ## 8. PROBLEMA SECUNDARIO: REPINTADO ### 8.1 Síntomas - Ventana no se refresca al volver a primer plano (Alt+Tab) - Partes de la ventana quedan sin pintar si estaban tapadas - El programa parece "colgado" pero solo está sin repintar ### 8.2 Causa Las optimizaciones de CPU (SDL_WaitEventTimeout) son demasiado agresivas. No se detectan eventos de ventana como: - `SDL_WINDOWEVENT_EXPOSED` - Ventana necesita repintarse - `SDL_WINDOWEVENT_FOCUS_GAINED` - Ventana ganó focus del sistema ### 8.3 Solución Añadir manejo de estos eventos en el backend SDL2: ```zig // En el event loop .windowevent => |we| { switch (we.event) { SDL_WINDOWEVENT_EXPOSED, SDL_WINDOWEVENT_FOCUS_GAINED, SDL_WINDOWEVENT_RESTORED, SDL_WINDOWEVENT_SHOWN => { needs_redraw = true; }, // ... } } ``` **Nota**: Este problema se abordará DESPUÉS de arreglar el sistema de focus. --- ## 9. REFERENCIAS - [microui source](https://github.com/rxi/microui) - [Gio input architecture](https://gioui.org/doc/architecture/input) - [Gio focus newsletter](https://gioui.org/news/2022-02) - Conversación de diseño: 2025-12-11/12