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
13 KiB
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_outcalculatePrevCell- navegación básica, wrap, tab_outtoggleSort- ciclo de direcciones, cambio de columnadetectDoubleClick- detección correcta de doble-clickblendColor- mezcla de coloresstartsWithIgnoreCase- 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únsrc/widgets/advanced_table/- AdvancedTable completosrc/widgets/virtual_advanced_table/- VirtualAdvancedTable completodocs/ADVANCED_TABLE_DESIGN.md- Diseño original (histórico)