zcatgui/docs/TABLES_ARCHITECTURE.md
reugenio e8b4c98d4a docs: Add TABLES_ARCHITECTURE.md - definitive reference for table widgets
Complete documentation covering:
- When to use AdvancedTable vs VirtualAdvancedTable
- Architecture diagram (table_core.zig as single source)
- How to extend functionality (always in table_core.zig)
- Complete API reference for table_core.zig
- Code examples and model differences
2025-12-27 01:58:57 +01:00

13 KiB

Arquitectura de Tablas en zcatgui

Documento de referencia definitivo para entender, usar y extender los widgets de tabla. Actualizado: 2025-12-27


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                              │   │
│  │                                                                  │   │
│  │  LÓGICA COMÚN (un solo lugar para modificar):                   │   │
│  │  - calculateNextCell() / calculatePrevCell()  [Tab navigation]  │   │
│  │  - toggleSort()                               [Ordenación]      │   │
│  │  - handleEditingKeyboard()                    [Edición celda]   │   │
│  │  - detectDoubleClick()                        [Doble-click]     │   │
│  │  - drawCellActiveIndicator()                  [Renderizado]     │   │
│  │  - drawEditingOverlay()                       [Overlay edición] │   │
│  │  - blendColor(), startsWithIgnoreCase()       [Utilidades]      │   │
│  │                                                                  │   │
│  │  TIPOS COMPARTIDOS:                                              │   │
│  │  - TabNavigateResult, CellPosition                               │   │
│  │  - SortDirection, SortToggleResult                               │   │
│  │  - TableColors, CellRenderInfo, EditState                        │   │
│  │  - EditKeyboardResult, DoubleClickState                          │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘

Estructura de Archivos

src/widgets/
├── table_core.zig                    # LÓGICA COMÚN - modificar aquí
│
├── advanced_table/                   # Tabla con datos en memoria
│   ├── advanced_table.zig            # Widget principal
│   ├── state.zig                     # Estado (usa table_core)
│   ├── types.zig                     # Tipos específicos
│   └── schema.zig                    # Definición de columnas
│
└── virtual_advanced_table/           # Tabla con datos paginados
    ├── virtual_advanced_table.zig    # Widget principal
    ├── state.zig                     # Estado (usa table_core)
    ├── types.zig                     # Tipos específicos
    └── data_provider.zig             # Interface para datos

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.

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.

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
}

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

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

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

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

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

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

pub const EditKeyboardResult = struct {
    committed: bool,      // Enter presionado
    cancelled: bool,      // Escape 2x
    reverted: bool,       // Escape 1x (revertir a original)
    navigate_next: bool,  // Tab
    navigate_prev: bool,  // Shift+Tab
    text_changed: bool,   // Texto modificado
};

/// Procesa teclado en modo edición
pub fn handleEditingKeyboard(ctx, edit_buffer, edit_len, edit_cursor, escape_count, original_text) EditKeyboardResult

Renderizado

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

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

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

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:

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

Historial de Cambios

Fecha Cambio
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)