From cf2f91f8bc98ebc21f397fc91ff44ff6b28a2c41 Mon Sep 17 00:00:00 2001 From: "R.Eugenio" Date: Sat, 27 Dec 2025 17:37:04 +0100 Subject: [PATCH] refactor(tables): FASE 4 - Add DataSource adapters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add MemoryDataSource for AdvancedTable (in-memory ArrayList) and PagedDataSource for VirtualAdvancedTable (paged DB data). Both implement TableDataSource interface from table_core.zig, enabling unified rendering patterns while respecting memory ownership. New files: - advanced_table/datasource.zig - MemoryDataSource - virtual_advanced_table/paged_datasource.zig - PagedDataSource 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/widgets/advanced_table/advanced_table.zig | 4 + src/widgets/advanced_table/datasource.zig | 126 ++++++++++++ .../paged_datasource.zig | 184 ++++++++++++++++++ .../virtual_advanced_table.zig | 2 + 4 files changed, 316 insertions(+) create mode 100644 src/widgets/advanced_table/datasource.zig create mode 100644 src/widgets/virtual_advanced_table/paged_datasource.zig diff --git a/src/widgets/advanced_table/advanced_table.zig b/src/widgets/advanced_table/advanced_table.zig index 5c7002b..696ae77 100644 --- a/src/widgets/advanced_table/advanced_table.zig +++ b/src/widgets/advanced_table/advanced_table.zig @@ -47,6 +47,10 @@ pub const state = @import("state.zig"); pub const AdvancedTableState = state.AdvancedTableState; pub const AdvancedTableResult = state.AdvancedTableResult; +// Re-export datasource +pub const datasource = @import("datasource.zig"); +pub const MemoryDataSource = datasource.MemoryDataSource; + // Re-export table_core types pub const NavigateDirection = table_core.NavigateDirection; diff --git a/src/widgets/advanced_table/datasource.zig b/src/widgets/advanced_table/datasource.zig new file mode 100644 index 0000000..e3e8753 --- /dev/null +++ b/src/widgets/advanced_table/datasource.zig @@ -0,0 +1,126 @@ +//! MemoryDataSource - Adaptador de TableDataSource para datos en memoria +//! +//! Wrappea AdvancedTableState + columnas para implementar la interfaz +//! TableDataSource de table_core. Permite que AdvancedTable use el mismo +//! patrón de renderizado que VirtualAdvancedTable. + +const std = @import("std"); +const table_core = @import("../table_core.zig"); +const state_mod = @import("state.zig"); +const schema_mod = @import("schema.zig"); +const types = @import("types.zig"); + +const AdvancedTableState = state_mod.AdvancedTableState; +const ColumnDef = schema_mod.ColumnDef; +const CellValue = types.CellValue; +const Row = types.Row; +const TableDataSource = table_core.TableDataSource; + +// ============================================================================= +// MemoryDataSource +// ============================================================================= + +/// Adaptador que implementa TableDataSource para datos en memoria (ArrayList) +/// Usa el patrón getCellValueInto para escritura segura en buffer proporcionado +pub const MemoryDataSource = struct { + /// Referencia al estado de la tabla + state: *AdvancedTableState, + + /// Definiciones de columnas (para mapear índice -> nombre) + columns: []const ColumnDef, + + /// Buffer interno para formateo (evita allocaciones) + format_buffer: [256]u8 = undefined, + + const Self = @This(); + + /// Crea un nuevo MemoryDataSource + pub fn init(state: *AdvancedTableState, columns: []const ColumnDef) Self { + return .{ + .state = state, + .columns = columns, + }; + } + + // ========================================================================= + // Implementación de TableDataSource + // ========================================================================= + + /// Retorna el número total de filas + pub fn getRowCount(self: *Self) usize { + return self.state.getRowCount(); + } + + /// Escribe el valor de una celda en el buffer proporcionado + /// Retorna slice del buffer con el contenido + pub fn getCellValueInto(self: *Self, row: usize, col: usize, buf: []u8) []const u8 { + // Validar índices + if (col >= self.columns.len) return ""; + + // Obtener fila + const row_data = self.state.getRowConst(row) orelse return ""; + + // Obtener nombre de columna + const col_name = self.columns[col].name; + + // Obtener valor de celda + const cell_value = row_data.get(col_name); + + // Formatear en el buffer proporcionado + return cell_value.format(buf); + } + + /// Retorna el ID único de una fila + /// Para datos en memoria, usamos el índice como ID + /// TODO: Si Row tuviera campo 'id', lo usaríamos + pub fn getRowId(self: *Self, row: usize) i64 { + _ = self; + // Por ahora usamos el índice como ID + // En el futuro, Row podría tener un campo 'id' explícito + return @intCast(row); + } + + /// Verifica si una celda es editable + pub fn isCellEditable(self: *Self, row: usize, col: usize) bool { + _ = row; + + // Verificar configuración de columna + if (col >= self.columns.len) return false; + return self.columns[col].editable; + } + + /// Invalida cache (no aplica para datos en memoria) + pub fn invalidate(self: *Self) void { + _ = self; + // No hay cache que invalidar en datos en memoria + } + + // ========================================================================= + // Conversión a TableDataSource + // ========================================================================= + + /// Crea TableDataSource vtable para este adaptador + pub fn toDataSource(self: *Self) TableDataSource { + return table_core.makeTableDataSource(Self, self); + } +}; + +// ============================================================================= +// Tests +// ============================================================================= + +test "MemoryDataSource basic" { + // Test básico de que compila y los tipos son correctos + const columns = [_]ColumnDef{ + .{ .name = "id", .title = "ID", .width = 50 }, + .{ .name = "name", .title = "Name", .width = 200 }, + }; + + var state = AdvancedTableState.init(std.testing.allocator); + defer state.deinit(); + + var ds = MemoryDataSource.init(&state, &columns); + const tds = ds.toDataSource(); + + try std.testing.expectEqual(@as(usize, 0), tds.getRowCount()); +} diff --git a/src/widgets/virtual_advanced_table/paged_datasource.zig b/src/widgets/virtual_advanced_table/paged_datasource.zig new file mode 100644 index 0000000..1a2da5a --- /dev/null +++ b/src/widgets/virtual_advanced_table/paged_datasource.zig @@ -0,0 +1,184 @@ +//! PagedDataSource - Adaptador de TableDataSource para datos paginados +//! +//! Wrappea VirtualAdvancedTableState + DataProvider para implementar la interfaz +//! TableDataSource de table_core. Permite unificar el pattern de renderizado +//! entre AdvancedTable (memoria) y VirtualAdvancedTable (paginado). + +const std = @import("std"); +const table_core = @import("../table_core.zig"); +const state_mod = @import("state.zig"); +const types = @import("types.zig"); +const data_provider_mod = @import("data_provider.zig"); + +const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState; +const ColumnDef = types.ColumnDef; +const RowData = types.RowData; +const DataProvider = data_provider_mod.DataProvider; +const TableDataSource = table_core.TableDataSource; + +// ============================================================================= +// PagedDataSource +// ============================================================================= + +/// Adaptador que implementa TableDataSource para datos paginados (ventana virtual). +/// Lee valores desde current_window en el state, que es propiedad del DataProvider. +pub const PagedDataSource = struct { + /// Referencia al estado de la tabla (contiene current_window) + state: *VirtualAdvancedTableState, + + /// DataProvider subyacente (para getRowId cuando fuera de ventana) + provider: ?DataProvider, + + /// Definiciones de columnas (para validar índices) + columns: []const ColumnDef, + + const Self = @This(); + + /// Crea un nuevo PagedDataSource + pub fn init( + state: *VirtualAdvancedTableState, + columns: []const ColumnDef, + provider: ?DataProvider, + ) Self { + return .{ + .state = state, + .columns = columns, + .provider = provider, + }; + } + + // ========================================================================= + // Implementación de TableDataSource + // ========================================================================= + + /// Retorna el número total de filas (filtered count) + pub fn getRowCount(self: *Self) usize { + // Usar conteo filtrado si está disponible + const count_info = self.state.getDisplayCount(); + return count_info.value; + } + + /// Escribe el valor de una celda en el buffer proporcionado. + /// El row es índice global, se convierte a índice de ventana. + /// Retorna slice del buffer con el contenido. + pub fn getCellValueInto(self: *Self, row: usize, col: usize, buf: []u8) []const u8 { + // Validar columna + if (col >= self.columns.len) return ""; + + // Convertir índice global a índice de ventana + const window_idx = self.state.globalToWindowIndex(row) orelse { + // Fila fuera de ventana actual - retornar vacío + // (el caller debería asegurarse de pedir solo filas visibles) + return ""; + }; + + // Obtener fila de la ventana + if (window_idx >= self.state.current_window.len) return ""; + const row_data = self.state.current_window[window_idx]; + + // Obtener valor de la columna + if (col >= row_data.values.len) return ""; + const value = row_data.values[col]; + + // Copiar al buffer proporcionado + const copy_len = @min(value.len, buf.len); + @memcpy(buf[0..copy_len], value[0..copy_len]); + return buf[0..copy_len]; + } + + /// Retorna el ID único de una fila. + /// Si está en ventana, usa window data. Si no, consulta al provider. + pub fn getRowId(self: *Self, row: usize) i64 { + // Intentar obtener de la ventana + if (self.state.globalToWindowIndex(row)) |window_idx| { + if (window_idx < self.state.current_window.len) { + return self.state.current_window[window_idx].id; + } + } + + // Fallback al provider si está disponible + if (self.provider) |provider| { + return provider.getRowId(row) orelse -1; + } + + // Si no hay provider, usar índice como fallback + return @intCast(row); + } + + /// Verifica si una celda es editable. + /// TODO: Podría usar metadata de columnas cuando esté disponible. + pub fn isCellEditable(self: *Self, row: usize, col: usize) bool { + _ = row; + // Por ahora, todas las columnas son no editables por defecto + // La edición se maneja a nivel de VirtualAdvancedTable + if (col >= self.columns.len) return false; + return false; // VirtualAdvancedTable maneja edición de forma específica + } + + /// Invalida cache. + /// Delega al state para forzar refetch. + pub fn invalidate(self: *Self) void { + self.state.invalidateWindow(); + } + + // ========================================================================= + // Conversión a TableDataSource + // ========================================================================= + + /// Crea TableDataSource vtable para este adaptador + pub fn toDataSource(self: *Self) TableDataSource { + return table_core.makeTableDataSource(Self, self); + } +}; + +// ============================================================================= +// Tests +// ============================================================================= + +test "PagedDataSource basic" { + // Test básico de que compila y los tipos son correctos + const columns = [_]ColumnDef{ + .{ .name = "id", .title = "ID", .width = 50 }, + .{ .name = "name", .title = "Name", .width = 200 }, + }; + + var state = VirtualAdvancedTableState{}; + var ds = PagedDataSource.init(&state, &columns, null); + const tds = ds.toDataSource(); + + // Sin datos, row count es 0 + try std.testing.expectEqual(@as(usize, 0), tds.getRowCount()); +} + +test "PagedDataSource getCellValueInto" { + const columns = [_]ColumnDef{ + .{ .name = "id", .title = "ID", .width = 50 }, + .{ .name = "name", .title = "Name", .width = 200 }, + }; + + // Crear datos de prueba + const values1 = [_][]const u8{ "1", "Alice" }; + const values2 = [_][]const u8{ "2", "Bob" }; + const rows = [_]RowData{ + .{ .id = 100, .values = &values1 }, + .{ .id = 101, .values = &values2 }, + }; + + var state = VirtualAdvancedTableState{}; + state.current_window = &rows; + state.window_start = 10; // Ventana empieza en índice global 10 + state.filtered_count = .{ .value = 100, .state = .ready }; + + var ds = PagedDataSource.init(&state, &columns, null); + + // Buffer para valores + var buf: [256]u8 = undefined; + + // Fila en ventana (global 10 = window 0) + const val1 = ds.getCellValueInto(10, 1, &buf); + try std.testing.expectEqualStrings("Alice", val1); + + // Fila fuera de ventana + const val2 = ds.getCellValueInto(0, 0, &buf); + try std.testing.expectEqualStrings("", val2); +} diff --git a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig index d5f6dd5..8b5eb2f 100644 --- a/src/widgets/virtual_advanced_table/virtual_advanced_table.zig +++ b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig @@ -26,6 +26,7 @@ pub const types = @import("types.zig"); pub const data_provider = @import("data_provider.zig"); pub const state_mod = @import("state.zig"); pub const cell_editor = @import("cell_editor.zig"); +pub const paged_datasource = @import("paged_datasource.zig"); // Tipos principales pub const RowData = types.RowData; @@ -41,6 +42,7 @@ pub const CellId = types.CellId; pub const CellGeometry = types.CellGeometry; pub const DataProvider = data_provider.DataProvider; +pub const PagedDataSource = paged_datasource.PagedDataSource; pub const CellEditorColors = cell_editor.CellEditorColors; pub const CellEditorResult = cell_editor.CellEditorResult; pub const NavigateDirection = cell_editor.NavigateDirection;