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>
218 lines
7.8 KiB
Markdown
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)*
|