# Sistema de Focus - Resolucion Final (2025-12-11) > **ESTADO**: RESUELTO > **Fecha resolucion**: 2025-12-11 ~19:00 > **Probado en**: zsimifactu (tabla lista clientes + panel detalle) --- ## RESUMEN EJECUTIVO El sistema de focus de zcatgui tenia dos bugs que impedian la navegacion por teclado al iniciar una aplicacion: 1. **Bug 1**: `selected_col` se inicializaba a -1, causando que `selectedCell()` retornara null 2. **Bug 2**: Sin celda seleccionada valida, los cambios de seleccion no se propagaban **Solucion**: Inicializar `selected_row` y `selected_col` a 0 cuando la tabla tiene datos. --- ## ANALISIS DEL PROBLEMA ### Sintoma - Al iniciar el programa, la tabla mostraba focus visual (borde azul) - Las flechas del teclado NO movian la seleccion - Al hacer clic con raton, todo funcionaba correctamente ### Investigacion (debug prints) ``` TABLE handleKeyboard: nav=down, selected_row=0 DOWN: MOVED to row 1 AFTER tableRect[frame=7]: selection_changed=true, selected_row=1, selected_col=-1 SELECTION_CHANGED[frame=7]: selectedCell() returned null! <-- AQUI EL BUG SYNC[frame=8]: DM idx=0, table row=1 -> forcing to 0 <-- DM no se actualizo ``` ### Causa raiz 1. `TableState` inicializa `selected_row = -1` y `selected_col = -1` 2. El teclado solo modifica `selected_row`, no `selected_col` 3. `selectedCell()` retorna `null` si cualquiera es < 0 4. Sin celda valida, el DataManager no se notifica del cambio 5. En el siguiente frame, el SYNC del DataManager restaura `selected_row` a 0 ### Por que funcionaba con clic El clic llama a `selectCell(row, col)` que inicializa AMBOS valores correctamente. --- ## SOLUCION IMPLEMENTADA ### Archivo: `zcatgui/src/widgets/table.zig` En `tableRectFull()`, despues de verificar que hay datos: ```zig // Ensure valid selection if table has data // Without this, selected_row/col stay at -1 until user clicks, // which breaks keyboard navigation and selectedCell() returns null if (state.row_count > 0 and columns.len > 0) { if (state.selected_row < 0) state.selected_row = 0; if (state.selected_col < 0) state.selected_col = 0; } ``` ### Por que esta solucion es correcta - Es logica: si una tabla tiene datos, debe tener una celda seleccionada por defecto - Se hace en la libreria, no en la aplicacion (principio de resolver en origen) - No rompe compatibilidad: las apps que ya usaban clic siguen funcionando - Beneficia a todas las apps que usen zcatgui --- ## OTROS CAMBIOS REALIZADOS (durante investigacion) ### 1. Focus implicito (conservar) **Archivo**: `zcatgui/src/core/focus.zig` `hasFocus()` y `getFocused()` ahora retornan el primer widget si `focused_index == null`: - Permite que widgets respondan al teclado desde el primer frame - `endFrame()` convierte el focus implicito en explicito ### 2. Separacion registration_group / active_group (conservar) **Archivos**: `focus.zig`, `context.zig` - `registration_group`: donde se registran widgets durante draw - `active_group`: grupo con focus de teclado (solo cambia con F6/clic) ### 3. Test de input corregido (conservar) **Archivo**: `zcatgui/src/core/input.zig` El test `navKeyPressed` usaba `setKeyState()` pero debia usar `handleKeyEvent()`. --- ## ESTADO DE PRUEBAS | Funcionalidad | Estado | Notas | |---------------|--------|-------| | Focus visual exclusivo | OK | Solo un panel con borde azul | | F6 cambia entre paneles | OK | | | Clic cambia focus | OK | | | Flechas mueven seleccion tabla | OK | Ahora funciona desde inicio | | Tab entre TextInputs | OK | | | Seleccion se propaga a DataManager | OK | Panel detalle se actualiza | | CPU idle ~0% | OK | SDL_WaitEventTimeout | --- ## ARQUITECTURA FINAL DEL SISTEMA DE FOCUS ``` FocusSystem | +---------------+---------------+ | | FocusGroup 1 FocusGroup 2 (Panel Lista) (Panel Detalle) | | Table widget TextInput widgets | | - registerFocusable() - registerFocusable() - hasFocus() -> true/false - hasFocus() -> true/false ``` ### Flujo por frame: 1. `beginFrame()` - limpia registros de widgets 2. `setRegistrationGroup(1)` - panel 1 registra sus widgets 3. `setRegistrationGroup(2)` - panel 2 registra sus widgets 4. Widgets preguntan `hasFocus()` durante draw 5. `endFrame()` - valida focus, procesa Tab pendiente ### Invariantes: - Solo UN grupo tiene `active_group` a la vez - El primer widget del grupo activo tiene focus implicito si `focused_index == null` - `selectedCell()` requiere `selected_row >= 0` Y `selected_col >= 0` --- ## LECCION APRENDIDA **Siempre inicializar estado completo, no parcial.** El bug surgio porque `selected_row` se manejaba correctamente pero `selected_col` se ignoraba. Cualquier funcion que dependa de multiples campos (como `selectedCell()`) fallara si alguno no esta inicializado. --- ## WIDGETS ADAPTADOS AL SISTEMA DE FOCUS Todos los widgets interactivos fueron revisados y adaptados al nuevo sistema de focus. ### Patrón de integración Cada widget interactivo debe seguir este patrón: ```zig pub fn widgetRect(ctx: *Context, bounds: Layout.Rect, state: *WidgetState, ...) Result { // 1. Generar ID único basado en dirección del state const widget_id: u64 = @intFromPtr(state); // 2. Registrar en el grupo de focus activo ctx.registerFocusable(widget_id); // 3. Al hacer clic, solicitar focus if (clicked) { ctx.requestFocus(widget_id); } // 4. Verificar si tiene focus const has_focus = ctx.hasFocus(widget_id); state.focused = has_focus; // 5. Solo procesar teclado si tiene focus if (has_focus) { // ... manejo de teclas } } ``` ### Widgets adaptados (integrados con FocusSystem) | Widget | Archivo | Estado | Notas | |--------|---------|--------|-------| | **Table** | `table.zig` | Adaptado | Fix principal - inicializa selected_row/col a 0 | | **TextInput** | `text_input.zig` | Correcto | Ya usaba el patrón correcto | | **NumberEntry** | `numberentry.zig` | Adaptado | Añadido registerFocusable, requestFocus, hasFocus | | **TextArea** | `textarea.zig` | Adaptado | Añadido registerFocusable, requestFocus, hasFocus | | **Select** | `select.zig` | Adaptado | Añadido campo focused, integración completa | | **Radio** | `radio.zig` | Adaptado | Reemplazado focus manual por FocusSystem | | **Slider** | `slider.zig` | Adaptado | Reemplazado focus manual por FocusSystem | | **Tabs** | `tabs.zig` | Adaptado | Navegación por teclado solo cuando tiene focus | ### Widgets que NO requieren adaptación | Widget | Motivo | |--------|--------| | **Menu** | Modal - cuando está abierto captura toda la entrada | | **Checkbox** | Solo responde a clic, no mantiene focus persistente | | **Switch** | Solo responde a clic, no mantiene focus persistente | | **Button** | Solo responde a clic, no mantiene focus persistente | | **Widgets display-only** | label, badge, progress, icon, image, etc. - no interactivos | | **Overlays** | modal, tooltip, toast - manejan su propia modalidad | ### Categorización de widgets **Widgets con focus persistente** (necesitan FocusSystem): - Widgets de entrada de datos (TextInput, NumberEntry, TextArea) - Widgets de selección con teclado (Table, Select, Radio, Tabs) - Widgets de valor con teclado (Slider) **Widgets sin focus persistente** (NO necesitan FocusSystem): - Widgets de acción única (Button, Checkbox, Switch) - Widgets de solo visualización (Label, Progress, Badge, Icon) - Widgets modales/overlay (Menu, Modal, Tooltip, Toast) --- *Documento actualizado: 2025-12-11* *Autores: Arno + Claude (Opus 4.5)*