zcatgui/docs/FOCUS_TRANSITION_2025-12-11.md
reugenio e0d7e99bb6 fix: Corregir nombre de usuario Arno → R.Eugenio
Arno es el nombre de la carpeta/servidor, no del usuario.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 10:39:22 +01:00

218 lines
7.8 KiB
Markdown

# 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: R.Eugenio + Claude (Opus 4.5)*