504 lines
19 KiB
Markdown
504 lines
19 KiB
Markdown
# Arquitectura de Tablas en zcatgui
|
|
|
|
> **Documento de referencia definitivo** para entender, usar y extender los widgets de tabla.
|
|
> Actualizado: 2025-12-27 (FASES 0-6 completadas - unificación total)
|
|
|
|
---
|
|
|
|
## 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] │ │
|
|
│ │ │ │
|
|
│ │ ESTADO EMBEBIDO (FASE 5): │ │
|
|
│ │ - NavigationState [active_col, scroll, double_click]│ │
|
|
│ │ - CellEditState [editing, buffer, cursor] │ │
|
|
│ │ │ │
|
|
│ │ SCROLLBARS UNIFICADOS (FASE 6): │ │
|
|
│ │ - drawVerticalScrollbar() [Scrollbar vertical] │ │
|
|
│ │ - drawHorizontalScrollbar() [Scrollbar horizontal] │ │
|
|
│ │ - VerticalScrollbarParams [Configuración vertical] │ │
|
|
│ │ - HorizontalScrollbarParams [Configuración horizontal] │ │
|
|
│ │ │ │
|
|
│ │ 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 │ │
|
|
│ │ - 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 nav: NavigationState)
|
|
│ ├── 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 nav: NavigationState)
|
|
├── 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 | FASE 6: Scrollbars unificados (drawVerticalScrollbar, drawHorizontalScrollbar) |
|
|
| 2025-12-27 | FASE 5: NavigationState embebido en ambos States |
|
|
| 2025-12-27 | FASE 4.5: AdvancedTable usa drawRowsWithDataSource (sin bucle for propio) |
|
|
| 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)
|