diff --git a/src/widgets/table_core.zig b/src/widgets/table_core.zig index 2e3dfed..6b595b9 100644 --- a/src/widgets/table_core.zig +++ b/src/widgets/table_core.zig @@ -938,6 +938,118 @@ pub fn toggleSort( }; } +// ============================================================================= +// TableDataSource Interface (FASE 3) +// ============================================================================= +// +// ## Interfaz TableDataSource +// +// Abstrae el origen de datos para tablas, permitiendo que el mismo widget +// renderice datos desde memoria (AdvancedTable) o desde BD paginada (VirtualAdvancedTable). +// +// ### Protocolo de Memoria +// +// `getCellValueInto` escribe directamente en el buffer proporcionado por el widget. +// Esto elimina problemas de ownership: el widget controla la vida del buffer. +// +// ### Ejemplo de uso: +// ```zig +// var buf: [256]u8 = undefined; +// const value = data_source.getCellValueInto(row, col, &buf); +// // value es un slice de buf, válido mientras buf exista +// ``` + +/// Interfaz genérica para proveer datos a tablas +/// Usa vtable pattern para polimorfismo en runtime +pub const TableDataSource = struct { + ptr: *anyopaque, + vtable: *const VTable, + + pub const VTable = struct { + /// Retorna el número total de filas en el datasource + getRowCount: *const fn (ptr: *anyopaque) usize, + + /// Escribe el valor de una celda en el buffer proporcionado + /// Retorna el slice del buffer con el contenido escrito + /// Si la celda no existe o está vacía, retorna "" + getCellValueInto: *const fn (ptr: *anyopaque, row: usize, col: usize, buf: []u8) []const u8, + + /// Retorna el ID único de una fila (para selección persistente) + /// NEW_ROW_ID (-1) indica fila nueva no guardada + getRowId: *const fn (ptr: *anyopaque, row: usize) i64, + + /// Verifica si una celda es editable (opcional, default true) + isCellEditable: ?*const fn (ptr: *anyopaque, row: usize, col: usize) bool = null, + + /// Invalida cache interno (para refresh) + invalidate: ?*const fn (ptr: *anyopaque) void = null, + }; + + // ========================================================================= + // Métodos de conveniencia + // ========================================================================= + + /// Obtiene el número de filas + pub fn getRowCount(self: TableDataSource) usize { + return self.vtable.getRowCount(self.ptr); + } + + /// Escribe valor de celda en buffer + pub fn getCellValueInto(self: TableDataSource, row: usize, col: usize, buf: []u8) []const u8 { + return self.vtable.getCellValueInto(self.ptr, row, col, buf); + } + + /// Obtiene ID de fila + pub fn getRowId(self: TableDataSource, row: usize) i64 { + return self.vtable.getRowId(self.ptr, row); + } + + /// Verifica si celda es editable + pub fn isCellEditable(self: TableDataSource, row: usize, col: usize) bool { + if (self.vtable.isCellEditable) |func| { + return func(self.ptr, row, col); + } + return true; // Default: todas editables + } + + /// Invalida cache + pub fn invalidate(self: TableDataSource) void { + if (self.vtable.invalidate) |func| { + func(self.ptr); + } + } + + /// Verifica si la fila es la ghost row (nueva) + pub fn isGhostRow(self: TableDataSource, row: usize) bool { + return self.getRowId(row) == NEW_ROW_ID; + } +}; + +/// Helper para crear TableDataSource desde un tipo concreto +/// El tipo T debe tener los métodos: getRowCount, getCellValueInto, getRowId +pub fn makeTableDataSource(comptime T: type, impl: *T) TableDataSource { + const vtable = comptime blk: { + var vt: TableDataSource.VTable = .{ + .getRowCount = @ptrCast(&T.getRowCount), + .getCellValueInto = @ptrCast(&T.getCellValueInto), + .getRowId = @ptrCast(&T.getRowId), + }; + // Métodos opcionales + if (@hasDecl(T, "isCellEditable")) { + vt.isCellEditable = @ptrCast(&T.isCellEditable); + } + if (@hasDecl(T, "invalidate")) { + vt.invalidate = @ptrCast(&T.invalidate); + } + break :blk vt; + }; + + return .{ + .ptr = impl, + .vtable = &vtable, + }; +} + // ============================================================================= // Tests // =============================================================================