zcatgui/docs/FOCUS_TRANSITION_2025-12-11.md
reugenio 3517a6f972 docs: Documentar sistema de focus completado y widgets adaptados
- Actualizar FOCUS_TRANSITION_2025-12-11.md con patrón de integración
- Actualizar CLAUDE.md: sección SISTEMA DE FOCUS - RESUELTO
- Widgets adaptados a FocusSystem:
  - numberentry.zig: registerFocusable, requestFocus, hasFocus
  - textarea.zig: registerFocusable, requestFocus, hasFocus
  - select.zig: campo focused, integración completa
  - radio.zig: reemplazado focus manual por FocusSystem
  - slider.zig: reemplazado focus manual por FocusSystem
  - tabs.zig: navegación teclado solo cuando tiene focus

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 18:50:37 +01:00

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


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:

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)