Widget AdvancedTable portado de Go (simifactu-fyne) a Zig. 2,526 LOC en 4 archivos, 370 tests pasan. Archivos: - types.zig (369 LOC): CellValue, ColumnType, RowState, TableColors - schema.zig (373 LOC): ColumnDef, TableSchema, DataStore interface - state.zig (762 LOC): Selection, editing, dirty tracking, snapshots - advanced_table.zig (1,022 LOC): Widget, rendering, keyboard Fases implementadas: 1. Core (types, schema, state) 2. Navigation (arrows, Tab, PgUp/Dn, Home/End, Ctrl+Home/End) 3. Cell Editing (F2/Enter start, Escape cancel, text input) 4. Sorting (header click, visual indicators) 5. Auto-CRUD (CREATE/UPDATE/DELETE detection on row change) 6. Row Operations (Ctrl+N insert, Ctrl+Delete remove) Fases diferidas (7-8): Lookup & Auto-fill, Callbacks avanzados ESTADO: Compilado y tests pasan. NO probado en uso real. REQUIERE: Aprobacion antes de tag de version. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
18 KiB
18 KiB
AdvancedTable - Documento de Diseño
Fecha: 2025-12-17 Basado en: Análisis de
simifactu-fyne/internal/ui/components/advanced_table/(~2,800 LOC Go) Target: zcatgui (Zig)
1. Resumen Ejecutivo
AdvancedTable es una tabla parametrizada de alto nivel que proporciona:
- Configuración schema-driven (sin callbacks hardcoded)
- Edición Excel-style (Entry overlay sobre celda)
- Auto-CRUD (detecta y ejecuta CREATE/UPDATE/DELETE automáticamente)
- Navegación teclado completa (arrows, Tab, Enter, Escape)
- Sorting por columna (click header, cycle asc/desc/restore)
- Visual states (normal, modified, new, deleted, error, selected)
- Lookup & Auto-fill (para campos de lookup de BD)
La idea es que cualquier tabla en la aplicación (WHO, líneas de documento, configuración, etc.) sea simplemente una instancia parametrizada de AdvancedTable.
2. Arquitectura Propuesta para zcatgui
2.1 Estructura de Archivos
src/widgets/advanced_table/
├── advanced_table.zig # Entry point, re-exports
├── types.zig # Tipos, enums, configs
├── schema.zig # TableSchema, ColumnDef
├── state.zig # AdvancedTableState
├── render.zig # Rendering visual
├── editing.zig # Edición in-situ
├── navigation.zig # Navegación teclado
├── sorting.zig # Sorting por columnas
├── row_ops.zig # Operaciones de filas (CRUD local)
├── autocrud.zig # Detección y ejecución Auto-CRUD
├── lookup.zig # Lookup y Auto-fill
└── callbacks.zig # Sistema de callbacks
2.2 Tipos Fundamentales
// types.zig
/// Tipos de datos para columnas
pub const ColumnType = enum {
string,
integer,
float,
money,
boolean,
date,
select, // Dropdown con opciones
lookup, // Busca en tabla relacionada
};
/// Estado de fila (dirty tracking)
pub const RowState = enum {
normal, // Sin cambios
modified, // Editado, pendiente de guardar
new, // Fila nueva, no existe en BD
deleted, // Marcado para borrar
error, // Error de validación
};
/// Estado de lock de fila (para VeriFacTu certified docs)
pub const RowLockState = enum {
unlocked, // Editable
locked, // Requiere password
read_only, // Solo lectura (certificado)
};
/// Resultado de validación
pub const ValidationResult = struct {
valid: bool,
message: []const u8 = "",
severity: enum { info, warning, error } = .error,
};
2.3 Schema de Tabla
// schema.zig
/// Definición de columna
pub const ColumnDef = struct {
/// Nombre interno (key en row map)
name: []const u8,
/// Título visible en header
title: []const u8,
/// Ancho en pixels
width: u32 = 100,
/// Tipo de datos
column_type: ColumnType = .string,
/// Es editable
editable: bool = true,
/// Es ordenable
sortable: bool = true,
/// Ancho mínimo
min_width: u32 = 40,
/// Alineación
align: enum { left, center, right } = .left,
// Lookup
enable_lookup: bool = false,
lookup_table: ?[]const u8 = null,
lookup_key_column: ?[]const u8 = null,
auto_fill_columns: ?[]const AutoFillMapping = null,
// Callbacks por columna (opcionales)
validator: ?*const fn(value: CellValue) ValidationResult = null,
formatter: ?*const fn(value: CellValue) []const u8 = null,
parser: ?*const fn(text: []const u8) ?CellValue = null,
};
/// Mapping para auto-fill después de lookup
pub const AutoFillMapping = struct {
source_field: []const u8, // Campo en tabla lookup
target_column: []const u8, // Columna destino
};
/// Schema completo de tabla
pub const TableSchema = struct {
/// Nombre de la tabla (para DataStore)
table_name: []const u8,
/// Columnas
columns: []const ColumnDef,
// Configuración visual
show_row_state_indicators: bool = true,
show_headers: bool = true,
header_height: u32 = 28,
row_height: u32 = 24,
alternating_rows: bool = true,
// Configuración de edición
allow_edit: bool = true,
allow_sorting: bool = true,
allow_row_operations: bool = true, // Ctrl+N, Delete, etc.
// Auto-CRUD
auto_crud_enabled: bool = true, // Detecta CREATE/UPDATE/DELETE al cambiar fila
always_show_empty_row: bool = false, // Fila vacía al final para entrada continua
// Row locking (para certificados)
support_row_locking: bool = false,
// DataStore (opcional, para Auto-CRUD)
data_store: ?*DataStore = null,
// Colors (opcional, override del theme)
colors: ?*const TableColors = null,
};
2.4 State de la Tabla
// state.zig
pub const AdvancedTableState = struct {
// Datos
rows: std.ArrayList(Row),
allocator: std.mem.Allocator,
// Selección
selected_row: i32 = -1,
selected_col: i32 = -1,
prev_selected_row: i32 = -1,
prev_selected_col: i32 = -1,
// Edición
editing: bool = false,
edit_buffer: [256]u8 = undefined,
edit_len: usize = 0,
edit_cursor: usize = 0,
original_value: ?CellValue = null,
escape_count: u8 = 0, // 1=revert, 2=cancel
// Sorting
sort_column: i32 = -1,
sort_ascending: bool = true,
original_order: ?std.ArrayList(Row) = null, // Para restore
// Scroll
scroll_row: usize = 0,
scroll_x: usize = 0,
// State maps (sparse - solo filas modificadas)
dirty_rows: std.AutoHashMap(usize, bool),
new_rows: std.AutoHashMap(usize, bool),
deleted_rows: std.AutoHashMap(usize, bool),
validation_errors: std.AutoHashMap(usize, bool),
// Snapshots para Auto-CRUD
row_snapshots: std.AutoHashMap(usize, Row),
// Focus
focused: bool = false,
// Debounce (para callbacks externos)
last_callback_time: i64 = 0,
pending_row_callback: ?usize = null,
pub fn init(allocator: std.mem.Allocator) AdvancedTableState { ... }
pub fn deinit(self: *AdvancedTableState) void { ... }
// Row operations
pub fn setRows(self: *AdvancedTableState, rows: []const Row) void { ... }
pub fn getRowCount(self: *AdvancedTableState) usize { ... }
pub fn getRow(self: *AdvancedTableState, index: usize) ?*Row { ... }
pub fn insertRow(self: *AdvancedTableState, index: usize) void { ... }
pub fn deleteRow(self: *AdvancedTableState, index: usize) void { ... }
pub fn moveRowUp(self: *AdvancedTableState, index: usize) void { ... }
pub fn moveRowDown(self: *AdvancedTableState, index: usize) void { ... }
// State queries
pub fn getRowState(self: *AdvancedTableState, index: usize) RowState { ... }
pub fn markDirty(self: *AdvancedTableState, index: usize) void { ... }
pub fn markNew(self: *AdvancedTableState, index: usize) void { ... }
pub fn markDeleted(self: *AdvancedTableState, index: usize) void { ... }
pub fn clearRowState(self: *AdvancedTableState, index: usize) void { ... }
// Snapshots (para Auto-CRUD)
pub fn captureSnapshot(self: *AdvancedTableState, index: usize) void { ... }
pub fn getSnapshot(self: *AdvancedTableState, index: usize) ?Row { ... }
pub fn clearSnapshot(self: *AdvancedTableState, index: usize) void { ... }
// Selection
pub fn selectCell(self: *AdvancedTableState, row: usize, col: usize) void { ... }
pub fn clearSelection(self: *AdvancedTableState) void { ... }
// Editing
pub fn startEditing(self: *AdvancedTableState, initial_value: []const u8) void { ... }
pub fn stopEditing(self: *AdvancedTableState) void { ... }
pub fn getEditText(self: *AdvancedTableState) []const u8 { ... }
// Sorting
pub fn toggleSort(self: *AdvancedTableState, col: usize) void { ... }
pub fn restoreOriginalOrder(self: *AdvancedTableState) void { ... }
};
2.5 DataStore Interface
// types.zig
/// Interface para persistencia (BD, archivo, API, etc.)
pub const DataStore = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
load: *const fn(ptr: *anyopaque) []Row,
save: *const fn(ptr: *anyopaque, row: *Row) anyerror!void,
delete: *const fn(ptr: *anyopaque, row: *Row) anyerror!void,
lookup: *const fn(ptr: *anyopaque, table: []const u8, key_col: []const u8, key_val: CellValue) ?Row,
};
pub fn load(self: DataStore) []Row {
return self.vtable.load(self.ptr);
}
pub fn save(self: DataStore, row: *Row) !void {
return self.vtable.save(self.ptr, row);
}
pub fn delete(self: DataStore, row: *Row) !void {
return self.vtable.delete(self.ptr, row);
}
pub fn lookup(self: DataStore, table: []const u8, key_col: []const u8, key_val: CellValue) ?Row {
return self.vtable.lookup(self.ptr, table, key_col, key_val);
}
};
3. Funcionalidades Detalladas
3.1 Edición Excel-Style
Comportamiento:
- Activar edición: F2, Enter, Spacebar, o tecla alfanumérica
- Tecla alfanumérica: Reemplaza contenido con esa letra, cursor al final
- F2/Enter/Space: Muestra valor actual, selecciona todo
- Durante edición:
- Arrow Up/Down: Commit + navega + auto-edit
- Tab: Commit + navega siguiente editable + auto-edit
- Shift+Tab: Commit + navega anterior + auto-edit
- Enter: Commit + navega abajo
- Escape (1): Revert al valor original (sigue editando)
- Escape (2): Cancel, sale de edición
- Commit: Valida, parsea, guarda en row, marca dirty
3.2 Navegación Teclado
| Tecla | Acción |
|---|---|
| ↑/↓/←/→ | Navegar entre celdas |
| Tab | Siguiente celda editable |
| Shift+Tab | Anterior celda editable |
| Enter | Iniciar edición o navegar abajo |
| F2 | Iniciar edición |
| Escape | Cancelar edición / Revert cambios |
| Ctrl+N | Insertar fila en posición actual |
| Ctrl+A | Agregar fila al final |
| Ctrl+B / Delete | Borrar fila actual |
| Ctrl+↑ | Mover fila arriba |
| Ctrl+↓ | Mover fila abajo |
| Home | Ir a primera columna |
| End | Ir a última columna |
| Ctrl+Home | Ir a primera fila |
| Ctrl+End | Ir a última fila |
| Page Up/Down | Scroll por página |
3.3 Auto-CRUD
Flujo:
- Usuario entra en fila →
captureSnapshot(rowIndex) - Usuario edita celdas → row modificado,
markDirty(rowIndex) - Usuario sale de fila (navega a otra) →
detectCRUDAction(rowIndex) - Según resultado:
- ActionNone: No hubo cambios reales → limpiar dirty flag
- ActionCreate: Snapshot vacío, current tiene datos →
DataStore.save()(INSERT) - ActionUpdate: Snapshot y current tienen datos diferentes →
DataStore.save()(UPDATE) - ActionDelete: Row marcado deleted o current vacío →
DataStore.delete()
Detección:
fn detectCRUDAction(state: *AdvancedTableState, row_index: usize) CRUDAction {
// Check deleted flag first (Ctrl+B)
if (state.deleted_rows.get(row_index)) |_| {
return .delete;
}
const snapshot = state.getSnapshot(row_index) orelse return .none;
const current = state.getRow(row_index) orelse return .none;
const was_empty = isRowEmpty(snapshot);
const is_empty = isRowEmpty(current);
const has_changes = rowHasChanges(snapshot, current);
if (was_empty and !is_empty) return .create;
if (!was_empty and !is_empty and has_changes) return .update;
if (!was_empty and is_empty) return .delete;
return .none;
}
3.4 Sorting
Ciclo: Click en header → Ascending → Click → Descending → Click → Restore original
Implementación:
- Primer sort en columna: Guardar orden original
- Sort usa índices indirectos (no modifica rows directamente)
- Después de sort: Sincronizar TODOS los state maps (dirty, new, deleted, snapshots, selection)
- Restore: Volver al orden guardado
3.5 Visual States
| Estado | Color Background | Indicador |
|---|---|---|
| Normal | Theme default | - |
| Modified | Amarillo suave | ● amarillo |
| New | Verde suave | ● verde |
| Deleted | Rojo suave | ● rojo |
| Error | Rojo intenso | ⚠ |
| Selected Cell | Azul intenso | - |
| Selected Row | Azul suave | - |
| Read-Only | Gris claro | 🔒 |
| Locked | Gris oscuro | 🔐 |
3.6 Lookup & Auto-fill
Caso de uso: Líneas de factura
- Usuario escribe código de producto
- Tabla busca en tabla
productosporcodigo - Si encuentra: Auto-rellena
descripcion,precio,iva_porc,re_porc - Si no encuentra: Callback
OnLookupNotFound(puede abrir diálogo crear producto)
Configuración en ColumnDef:
ColumnDef{
.name = "producto_codigo",
.title = "Código",
.enable_lookup = true,
.lookup_table = "productos",
.lookup_key_column = "codigo",
.auto_fill_columns = &[_]AutoFillMapping{
.{ .source_field = "descripcion", .target_column = "descripcion" },
.{ .source_field = "precio_venta", .target_column = "precio" },
.{ .source_field = "iva_porcentaje", .target_column = "iva_porc" },
.{ .source_field = "re_porcentaje", .target_column = "re_porc" },
},
}
4. API Pública
// advanced_table.zig
/// Dibujar AdvancedTable con schema y state
pub fn advancedTable(
ctx: *Context,
state: *AdvancedTableState,
schema: *const TableSchema,
) AdvancedTableResult;
/// Dibujar en rectángulo específico
pub fn advancedTableRect(
ctx: *Context,
bounds: Layout.Rect,
state: *AdvancedTableState,
schema: *const TableSchema,
colors: ?*const TableColors,
) AdvancedTableResult;
/// Resultado de interacción
pub const AdvancedTableResult = struct {
// Selección
selection_changed: bool = false,
selected_row: ?usize = null,
selected_col: ?usize = null,
// Edición
edit_started: bool = false,
edit_ended: bool = false,
cell_edited: bool = false,
// Sorting
sort_changed: bool = false,
sort_column: ?usize = null,
sort_ascending: bool = true,
// Row operations
row_inserted: bool = false,
row_deleted: bool = false,
row_moved: bool = false,
// Auto-CRUD (si habilitado)
crud_action: ?CRUDAction = null,
crud_error: ?[]const u8 = null,
// Lookup
lookup_triggered: bool = false,
lookup_found: bool = false,
};
5. Comparativa con Table actual
| Feature | Table actual | AdvancedTable |
|---|---|---|
| Config | Callbacks getCellData |
Schema-driven |
| Edición | Overlay básico | Excel-style completo |
| Auto-CRUD | Manual | Automático |
| Lookup | No | Sí |
| Calculated | No | Sí (futuro) |
| Row types | No | Sí (futuro) |
| Row locking | No | Sí |
| Sorting sync | Básico | Completo |
| State maps | Básico | Sparse optimizado |
| Callbacks | Legacy | Jerarquía Per-Column → Global → Default |
| Debounce | No | Sí (150ms default) |
6. Fases de Implementación
Fase 1: Core (~600 LOC)
- types.zig - Tipos básicos
- schema.zig - TableSchema, ColumnDef
- state.zig - AdvancedTableState
- advanced_table.zig - Rendering básico
Fase 2: Navegación (~300 LOC)
- navigation.zig - Keyboard navigation
- Integración con FocusSystem
Fase 3: Edición (~400 LOC)
- editing.zig - Entry overlay, commit/cancel
- Parsers y formatters por tipo
Fase 4: Sorting (~200 LOC)
- sorting.zig - Sort por columna
- Sync de state maps
Fase 5: Row Operations (~250 LOC)
- row_ops.zig - Insert/Delete/Move
- State map management
Fase 6: Auto-CRUD (~300 LOC)
- autocrud.zig - Detection & execution
- DataStore interface
- Snapshots
Fase 7: Lookup (~200 LOC)
- lookup.zig - Lookup & auto-fill
Fase 8: Callbacks (~150 LOC)
- callbacks.zig - Sistema jerárquico
- Debounce
Total estimado: ~2,400 LOC
7. Ejemplo de Uso
// Panel WHO List usando AdvancedTable
const who_schema = TableSchema{
.table_name = "who",
.columns = &[_]ColumnDef{
.{ .name = "id", .title = "ID", .width = 50, .editable = false },
.{ .name = "serie", .title = "Serie", .width = 80 },
.{ .name = "numero", .title = "Número", .width = 80, .column_type = .integer },
.{ .name = "fecha", .title = "Fecha", .width = 100, .column_type = .date },
.{ .name = "cliente_nif", .title = "NIF", .width = 100,
.enable_lookup = true,
.lookup_table = "clientes",
.lookup_key_column = "nif",
.auto_fill_columns = &[_]AutoFillMapping{
.{ .source_field = "nombre", .target_column = "cliente_nombre" },
.{ .source_field = "direccion", .target_column = "cliente_direccion" },
},
},
.{ .name = "cliente_nombre", .title = "Cliente", .width = 200 },
.{ .name = "base", .title = "Base", .width = 100, .column_type = .money, .align = .right },
.{ .name = "total", .title = "Total", .width = 100, .column_type = .money, .align = .right },
},
.auto_crud_enabled = true,
.show_row_state_indicators = true,
.data_store = &who_data_store,
};
var who_state = AdvancedTableState.init(allocator);
defer who_state.deinit();
// En el loop de UI:
const result = advancedTable(&ctx, &who_state, &who_schema);
if (result.selection_changed) {
// Actualizar panel de detalle
if (result.selected_row) |row_idx| {
loadDocumentDetail(who_state.getRow(row_idx));
}
}
if (result.crud_action) |action| {
switch (action) {
.create => showStatus("Documento creado", .success),
.update => showStatus("Documento guardado", .success),
.delete => showStatus("Documento borrado", .success),
.none => {},
}
}
8. Referencias
- Go Implementation:
/mnt/cello2/arno/re/recode/go/simifactu-fyne/internal/ui/components/advanced_table/ - zcatgui Table actual:
src/widgets/table/ - DVUI Audit:
docs/research/DVUI_AUDIT_2025-12-17.md