# Arquitectura de Tablas en zcatgui > **Documento de referencia definitivo** para entender, usar y extender los widgets de tabla. > Actualizado: 2025-12-27 (FASE 4.5 completada - rendering unificado) --- ## Resumen Ejecutivo zcatgui proporciona **dos widgets de tabla** que comparten lógica común: | Widget | Uso | Datos | |--------|-----|-------| | **AdvancedTable** | Tablas pequeñas/medianas | En memoria (ArrayList) | | **VirtualAdvancedTable** | Tablas grandes (60k+ filas) | Paginados via DataProvider | **Principio DRY:** Toda la lógica común está en `table_core.zig`. Los widgets adaptan esa lógica a su modelo de datos. --- ## Arquitectura de Capas ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ APLICACIÓN │ │ (zsimifactu, etc.) │ │ - Usa AdvancedTable o VirtualAdvancedTable según necesidad │ │ - Implementa DataProvider para VirtualAdvancedTable │ │ - Maneja lógica de negocio (guardar en BD, validar, etc.) │ └─────────────────────────────────────────────────────────────────────────┘ │ │ usa ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ WIDGETS DE TABLA │ │ │ │ ┌─────────────────────┐ ┌──────────────────────────┐ │ │ │ AdvancedTable │ │ VirtualAdvancedTable │ │ │ │ │ │ │ │ │ │ - Datos en memoria │ │ - Datos paginados │ │ │ │ - selected_row/col │ │ - selected_id + active_col│ │ │ │ - Ordenación local │ │ - Scroll virtual │ │ │ │ - Multi-selección │ │ - FilterBar integrada │ │ │ └──────────┬──────────┘ └────────────┬─────────────┘ │ │ │ │ │ │ │ ambos usan │ │ │ └──────────────┬───────────────────┘ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ table_core.zig │ │ │ │ │ │ │ │ RENDERING UNIFICADO (FASE 4.5): │ │ │ │ - drawRowsWithDataSource() [Dibuja filas con DataSource] │ │ │ │ - drawStateIndicator() [Indicador estado fila] │ │ │ │ - TableDataSource [Interface vtable] │ │ │ │ - MemoryDataSource [Adaptador datos memoria] │ │ │ │ - PagedDataSource [Adaptador datos paginados] │ │ │ │ │ │ │ │ LÓGICA COMÚN: │ │ │ │ - calculateNextCell() / calculatePrevCell() [Tab navigation] │ │ │ │ - toggleSort() [Ordenación] │ │ │ │ - handleEditingKeyboard() [Edición celda] │ │ │ │ - detectDoubleClick() [Doble-click] │ │ │ │ - blendColor(), startsWithIgnoreCase() [Utilidades] │ │ │ │ │ │ │ │ TIPOS COMPARTIDOS: │ │ │ │ - RowState (normal/modified/new/deleted/error) │ │ │ │ - TabNavigateResult, CellPosition, CellEditState │ │ │ │ - DrawRowsConfig, RowRenderColors, ColumnRenderDef │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## Estructura de Archivos ``` src/widgets/ ├── table_core.zig # LÓGICA COMÚN + RENDERING UNIFICADO │ ├── advanced_table/ # Tabla con datos en memoria │ ├── advanced_table.zig # Widget principal (usa drawRowsWithDataSource) │ ├── datasource.zig # MemoryDataSource (adaptador) │ ├── state.zig # Estado (embeds CellEditState) │ ├── types.zig # Tipos específicos │ └── schema.zig # Definición de columnas │ └── virtual_advanced_table/ # Tabla con datos paginados ├── virtual_advanced_table.zig # Widget principal (usa drawRowsWithDataSource) ├── paged_datasource.zig # PagedDataSource (adaptador) ├── state.zig # Estado (embeds CellEditState) ├── types.zig # Tipos específicos └── data_provider.zig # Interface para datos BD ``` --- ## Cuándo Usar Cada Widget ### AdvancedTable **Usar cuando:** - Datos caben en memoria (< 10,000 filas típicamente) - Necesitas ordenación/filtrado local rápido - Multi-selección de filas (Ctrl+Click, Shift+Click) - Los datos ya están cargados **Ejemplo:** Lista de tipos de IVA, categorías, configuraciones. ```zig const state = AdvancedTableState.init(allocator); defer state.deinit(); // Cargar datos try state.setRows(&my_rows); // En draw(): const result = advancedTable(ctx, rect, &state, &schema, colors); if (result.selection_changed) { // Manejar selección } ``` ### VirtualAdvancedTable **Usar cuando:** - Miles o millones de filas (60k+ poblaciones, logs, etc.) - Datos vienen de BD/API con paginación - Necesitas FilterBar integrada - Memoria limitada **Ejemplo:** Lista de poblaciones, historial de documentos. ```zig var state = VirtualAdvancedTableState{}; const provider = MyDataProvider.init(db); // En draw(): const result = virtualAdvancedTableRect(ctx, rect, &state, provider.toDataProvider(), config); if (result.selection_changed) { // Manejar selección } ``` --- ## TableDataSource: Rendering Unificado (FASE 4.5) A partir de la FASE 4.5, ambas tablas usan la misma función `drawRowsWithDataSource()` para renderizar filas. Esto se logra mediante el patrón **adaptador con vtable**. ### Interface TableDataSource ```zig pub const TableDataSource = struct { ptr: *anyopaque, vtable: *const VTable, pub const VTable = struct { getRowCount: *const fn (ptr: *anyopaque) usize, getCellValueInto: *const fn (ptr: *anyopaque, row: usize, col: usize, buf: []u8) []const u8, getRowId: *const fn (ptr: *anyopaque, row: usize) i64, isCellEditable: *const fn (ptr: *anyopaque, row: usize, col: usize) bool, invalidate: *const fn (ptr: *anyopaque) void, getRowState: ?*const fn (ptr: *anyopaque, row: usize) RowState = null, }; }; ``` ### Adaptadores | Adaptador | Tabla | Datos | |-----------|-------|-------| | `MemoryDataSource` | AdvancedTable | ArrayList en memoria | | `PagedDataSource` | VirtualAdvancedTable | Ventana paginada | ### Función de Rendering Unificada ```zig pub fn drawRowsWithDataSource( ctx: *Context, datasource: TableDataSource, config: DrawRowsConfig, cell_buffer: []u8, ) usize ``` **Características:** - Dibuja filas con colores por estado (modified, new, deleted, error) - Indicador de estado (columna izquierda con círculo de color) - Celda activa con highlight - Fila seleccionada con color diferenciado - Bordes de fila opcionales ### Cómo Crear un Nuevo Tipo de Tabla Si necesitas un tercer tipo de tabla (ej. streaming en tiempo real): 1. Crear adaptador que implemente `TableDataSource.VTable` 2. El rendering ya está hecho - solo provees datos via `getCellValueInto()` --- ## Cómo Extender la Funcionalidad ### Regla de Oro (Norma #7 DRY) > **Si la funcionalidad es común a ambas tablas, va en `table_core.zig`.** > Los widgets solo adaptan el resultado a su modelo de datos. ### Ejemplo: Añadir nueva navegación **PASO 1:** Añadir la lógica en `table_core.zig` ```zig // table_core.zig /// Calcula la celda de la siguiente página pub fn calculatePageDown( current_row: usize, current_col: usize, num_rows: usize, page_size: usize, ) CellPosition { const new_row = @min(current_row + page_size, num_rows - 1); return .{ .row = new_row, .col = current_col, .result = .navigated }; } ``` **PASO 2:** Usar en ambos states (adaptan a su modelo) ```zig // advanced_table/state.zig pub fn pageDown(self: *AdvancedTableState, page_size: usize) void { const pos = table_core.calculatePageDown( @intCast(self.selected_row), @intCast(self.selected_col), self.getRowCount(), page_size, ); self.selected_row = @intCast(pos.row); } // virtual_advanced_table/state.zig pub fn pageDown(self: *Self, page_size: usize) void { const current_row = self.getSelectedRow() orelse 0; const pos = table_core.calculatePageDown(current_row, self.active_col, total_rows, page_size); // Adaptar a scroll virtual... } ``` ### Ejemplo: Añadir nuevo renderizado ```zig // table_core.zig /// Dibuja indicador de celda con error de validación pub fn drawCellErrorIndicator( ctx: *Context, x: i32, y: i32, width: u32, height: u32, ) void { // Borde rojo ctx.pushCommand(Command.rectOutline(x, y, width, height, Color.rgb(255, 0, 0))); // Icono de error en esquina ctx.pushCommand(Command.text(x + width - 12, y + 2, "!", Color.rgb(255, 0, 0))); } ``` --- ## API de table_core.zig ### Navegación Tab ```zig /// Resultado de navegación pub const TabNavigateResult = enum { navigated, tab_out }; /// Posición de celda calculada pub const CellPosition = struct { row: usize, col: usize, result: TabNavigateResult }; /// Calcula siguiente celda (Tab) pub fn calculateNextCell(current_row, current_col, num_cols, num_rows, wrap) CellPosition /// Calcula celda anterior (Shift+Tab) pub fn calculatePrevCell(current_row, current_col, num_cols, num_rows, wrap) CellPosition ``` ### Ordenación ```zig pub const SortDirection = enum { none, ascending, descending }; pub const SortToggleResult = struct { column: ?usize, direction: SortDirection }; /// Calcula nuevo estado de ordenación al click en columna pub fn toggleSort(current_column, current_direction, clicked_column) SortToggleResult ``` ### Edición de Celda ```zig /// Dirección de navegación después de commit pub const NavigateDirection = enum { none, // Sin navegación next_cell, // Tab → siguiente celda prev_cell, // Shift+Tab → celda anterior next_row, // Enter/↓ → siguiente fila prev_row, // ↑ → fila anterior }; pub const EditKeyboardResult = struct { committed: bool, // Enter/Tab/flechas presionados cancelled: bool, // Escape 2x reverted: bool, // Escape 1x (revertir a original) navigate: NavigateDirection, // Dirección de navegación post-commit text_changed: bool, // Texto modificado handled: bool, // IMPORTANTE: evento fue procesado }; /// Procesa teclado en modo edición (DRY - un solo lugar) pub fn handleEditingKeyboard(ctx, edit_buffer, edit_len, edit_cursor, escape_count, original_text) EditKeyboardResult ``` > **IMPORTANTE:** El campo `handled` indica si table_core procesó el evento de teclado. > Esto es crítico para evitar doble procesamiento de Tab (ver "Lecciones Aprendidas"). ### Renderizado ```zig /// Dibuja indicador de celda activa pub fn drawCellActiveIndicator(ctx, x, y, width, height, row_bg, colors, has_focus) void /// Dibuja overlay de edición pub fn drawEditingOverlay(ctx, x, y, width, height, edit_text, cursor_pos, colors) void /// Dibuja texto de celda pub fn drawCellText(ctx, x, y, width, height, text, color, text_align) void ``` ### Utilidades ```zig /// Detecta doble-click pub fn detectDoubleClick(state, current_time, row, col) bool /// Mezcla dos colores pub fn blendColor(base, overlay, alpha) Color /// Compara strings case-insensitive pub fn startsWithIgnoreCase(haystack, needle) bool ``` --- ## Modelo de Datos ### AdvancedTable ```zig pub const AdvancedTableState = struct { // Datos rows: ArrayListUnmanaged(Row), // Selección (por ÍNDICE) selected_row: i32, // -1 = ninguna selected_col: i32, // Multi-selección selected_rows: [128]u8, // Bitset para 1024 filas // Edición editing: bool, edit_buffer: [256]u8, edit_cursor: usize, // Ordenación sort_column: i32, sort_direction: SortDirection, }; ``` ### VirtualAdvancedTable ```zig pub const VirtualAdvancedTableState = struct { // Selección (por ID, no índice) selected_id: ?i64, // ID del registro active_col: usize, // Columna activa // Ventana virtual scroll_offset: usize, // Offset en filas current_window: []RowData, // Datos visibles window_start: usize, // Índice inicial de ventana // Edición editing_cell: ?CellId, edit_buffer: [256]u8, row_dirty: bool, // Filtro filter_buf: [256]u8, active_chips: u16, // Bitset de chips activos }; ``` --- ## Tests Los tests de `table_core.zig` verifican la lógica común: ```bash cd zcatgui && zig build test ``` Tests incluidos: - `calculateNextCell` - navegación básica, wrap, tab_out - `calculatePrevCell` - navegación básica, wrap, tab_out - `toggleSort` - ciclo de direcciones, cambio de columna - `detectDoubleClick` - detección correcta de doble-click - `blendColor` - mezcla de colores - `startsWithIgnoreCase` - búsqueda case-insensitive --- ## Lecciones Aprendidas ### Bug: Color de fila alternando al presionar Tab (2025-12-27) **Síntoma:** Al navegar entre celdas con Tab, el color de la fila seleccionada alternaba entre azul (con focus) y marrón/gris (sin focus), independientemente de si se modificaba el contenido. **Causa raíz:** Tab se procesaba DOS veces: 1. Por VirtualAdvancedTable/CellEditor → navegación entre celdas 2. Por main.zig de la aplicación → cambio de focus entre widgets (`ctx.handleTabKey()`) Esto causaba que el focus del widget alternara incorrectamente cada frame. **Solución (3 partes):** 1. **table_core.zig:** Añadir campo `handled` a `EditKeyboardResult` para indicar que el evento fue procesado. 2. **VirtualAdvancedTable:** Verificar `navigate_direction != .none` antes de procesar Tab como `tab_out`. Si la tabla ya procesó Tab para navegación interna, no marcar `tab_out = true`. 3. **Aplicación (main.zig):** No llamar `ctx.handleTabKey()` cuando el panel activo maneja Tab internamente (ej: cuando `active_tab == .configuracion`). **Regla general:** > Cuando un widget procesa teclado internamente en `draw()`, la aplicación debe > respetar esa decisión y NO procesar el mismo evento a nivel global. **Código clave:** ```zig // VirtualAdvancedTable - handleKeyboard() .tab => { // IMPORTANTE: Solo si CellEditor no procesó Tab if (result.navigate_direction == .none) { result.tab_out = true; result.tab_shift = event.modifiers.shift; } }, ``` --- ## Historial de Cambios | Fecha | Cambio | |-------|--------| | 2025-12-27 | Fix bug colores alternando + campo `handled` en EditKeyboardResult | | 2025-12-27 | Refactorización DRY: lógica común movida a table_core.zig | | 2025-12-26 | table_core.zig creado con funciones de renderizado compartidas | | 2025-12-17 | VirtualAdvancedTable añadido para tablas grandes | | 2025-12-16 | AdvancedTable implementado con CRUD Excel-style | --- ## Referencias - `src/widgets/table_core.zig` - Código fuente de lógica común - `src/widgets/advanced_table/` - AdvancedTable completo - `src/widgets/virtual_advanced_table/` - VirtualAdvancedTable completo - `docs/ADVANCED_TABLE_DESIGN.md` - Diseño original (histórico)