zcatgui/docs/FOCUS_TRANSITION_2025-12-11.md
reugenio 7cde6370d8 fix: Sistema de focus rediseñado y funcionando
Cambios principales:
- Nuevo FocusSystem unificado en core/focus.zig
- Separación registration_group / active_group para multi-panel
- Focus implícito para primer widget del grupo activo
- Table inicializa selected_row/col a 0 cuando tiene datos
- Corregido test navKeyPressed (usaba setKeyState en vez de handleKeyEvent)

Bug resuelto: tabla no respondía a teclado sin clic previo
Causa: selected_col quedaba en -1, selectedCell() retornaba null

Documentación: docs/FOCUS_TRANSITION_2025-12-11.md

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 17:55:08 +01:00

5.1 KiB

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:

// 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.


Documento cerrado: 2025-12-11 ~19:00 Autores: Arno + Claude (Opus 4.5)