zcatgui/src/widgets/virtual_advanced_table/data_provider.zig
reugenio 66816bcbf1 feat(virtual_advanced_table): Add CRUD Excel-style editing state
- Rename VirtualList → VirtualAdvancedTable
- Add CellId and CellGeometry types
- Add editing state fields (editing_cell, original_value, escape_count, etc.)
- Add editing methods: startEditing, commitEdit, cancelEdit, handleEscape
- Add getCellGeometry() for overlay positioning
- Add row_dirty flag for change tracking
2025-12-26 14:45:32 +01:00

296 lines
9.7 KiB
Zig

//! DataProvider - Interface genérica para fuentes de datos
//!
//! Permite que VirtualAdvancedTable trabaje con cualquier fuente de datos
//! (SQLite, arrays, APIs, etc.) mediante un vtable pattern.
//!
//! Ejemplo de implementación:
//! ```zig
//! const WhoDataProvider = struct {
//! dm: *DataManager,
//!
//! pub fn toDataProvider(self: *WhoDataProvider) DataProvider {
//! return DataProvider.init(self, &vtable);
//! }
//!
//! const vtable = DataProvider.VTable{
//! .fetchWindow = fetchWindow,
//! .getTotalCount = getTotalCount,
//! // ...
//! };
//!
//! fn fetchWindow(ptr: *anyopaque, offset: usize, limit: usize) ![]const RowData {
//! const self: *WhoDataProvider = @ptrCast(@alignCast(ptr));
//! // Query BD con LIMIT/OFFSET
//! }
//! };
//! ```
const std = @import("std");
const types = @import("types.zig");
const RowData = types.RowData;
const CountInfo = types.CountInfo;
const SortDirection = types.SortDirection;
/// Interface genérica para proveedores de datos
pub const DataProvider = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
/// Obtiene una ventana de datos
/// Los datos retornados son válidos hasta el próximo fetchWindow
/// @param offset: índice del primer registro
/// @param limit: número máximo de registros a retornar
/// @return slice de RowData (propiedad del provider, no liberar)
fetchWindow: *const fn (
ptr: *anyopaque,
offset: usize,
limit: usize,
) error{OutOfMemory}![]const RowData,
/// Obtiene información del conteo total
/// Puede retornar estado parcial mientras calcula
getTotalCount: *const fn (ptr: *anyopaque) CountInfo,
/// Obtiene información del conteo filtrado
/// Puede retornar estado parcial mientras calcula
getFilteredCount: *const fn (ptr: *anyopaque) CountInfo,
/// Aplica filtro de búsqueda
/// @param filter: texto a buscar (vacío = sin filtro)
setFilter: *const fn (ptr: *anyopaque, filter: []const u8) void,
/// Aplica ordenación
/// @param column: nombre de columna (de ColumnDef.name)
/// @param direction: dirección de ordenación
setSort: *const fn (
ptr: *anyopaque,
column: []const u8,
direction: SortDirection,
) void,
/// Obtiene el ID del registro en un índice global
/// Útil para mantener selección después de scroll
/// @param index: índice global (no offset de ventana)
/// @return ID del registro o null si fuera de rango
getRowId: *const fn (ptr: *anyopaque, index: usize) ?i64,
/// Procesa tareas en background (conteos diferidos, etc.)
/// Llamar desde el main loop cuando hay tiempo idle
/// @return true si hay más trabajo pendiente
processBackgroundTasks: *const fn (ptr: *anyopaque) bool,
/// Invalida caché y fuerza recarga
invalidate: *const fn (ptr: *anyopaque) void,
/// Libera recursos del provider
deinit: *const fn (ptr: *anyopaque) void,
};
/// Crea un DataProvider desde una implementación concreta
pub fn init(ptr: anytype, vtable: *const VTable) DataProvider {
const Ptr = @TypeOf(ptr);
const ptr_info = @typeInfo(Ptr);
if (ptr_info != .pointer) {
@compileError("ptr must be a pointer");
}
return .{
.ptr = @ptrCast(ptr),
.vtable = vtable,
};
}
// =========================================================================
// Métodos wrapper para facilitar uso
// =========================================================================
/// Obtiene una ventana de datos
pub fn fetchWindow(self: DataProvider, offset: usize, limit: usize) ![]const RowData {
return self.vtable.fetchWindow(self.ptr, offset, limit);
}
/// Obtiene información del conteo total
pub fn getTotalCount(self: DataProvider) CountInfo {
return self.vtable.getTotalCount(self.ptr);
}
/// Obtiene información del conteo filtrado
pub fn getFilteredCount(self: DataProvider) CountInfo {
return self.vtable.getFilteredCount(self.ptr);
}
/// Aplica filtro de búsqueda
pub fn setFilter(self: DataProvider, filter: []const u8) void {
self.vtable.setFilter(self.ptr, filter);
}
/// Aplica ordenación
pub fn setSort(self: DataProvider, column: []const u8, direction: SortDirection) void {
self.vtable.setSort(self.ptr, column, direction);
}
/// Obtiene el ID del registro en un índice
pub fn getRowId(self: DataProvider, index: usize) ?i64 {
return self.vtable.getRowId(self.ptr, index);
}
/// Procesa tareas en background
pub fn processBackgroundTasks(self: DataProvider) bool {
return self.vtable.processBackgroundTasks(self.ptr);
}
/// Invalida caché
pub fn invalidate(self: DataProvider) void {
self.vtable.invalidate(self.ptr);
}
/// Libera recursos
pub fn deinit(self: DataProvider) void {
self.vtable.deinit(self.ptr);
}
};
// =============================================================================
// Tests
// =============================================================================
const testing = std.testing;
/// Provider de prueba para tests
const MockProvider = struct {
data: []const RowData,
total: usize,
filter_applied: bool = false,
sort_column: ?[]const u8 = null,
invalidated: bool = false,
pub fn toDataProvider(self: *MockProvider) DataProvider {
return DataProvider.init(self, &vtable);
}
const vtable = DataProvider.VTable{
.fetchWindow = fetchWindow,
.getTotalCount = getTotalCount,
.getFilteredCount = getFilteredCount,
.setFilter = setFilter,
.setSort = setSort,
.getRowId = getRowId,
.processBackgroundTasks = processBackgroundTasks,
.invalidate = invalidate,
.deinit = deinitFn,
};
fn fetchWindow(ptr: *anyopaque, offset: usize, limit: usize) ![]const RowData {
const self: *MockProvider = @ptrCast(@alignCast(ptr));
const end = @min(offset + limit, self.data.len);
if (offset >= self.data.len) return &.{};
return self.data[offset..end];
}
fn getTotalCount(ptr: *anyopaque) CountInfo {
const self: *MockProvider = @ptrCast(@alignCast(ptr));
return .{ .value = self.total, .state = .ready };
}
fn getFilteredCount(ptr: *anyopaque) CountInfo {
const self: *MockProvider = @ptrCast(@alignCast(ptr));
return .{ .value = self.data.len, .state = .ready };
}
fn setFilter(ptr: *anyopaque, _: []const u8) void {
const self: *MockProvider = @ptrCast(@alignCast(ptr));
self.filter_applied = true;
}
fn setSort(ptr: *anyopaque, column: []const u8, _: SortDirection) void {
const self: *MockProvider = @ptrCast(@alignCast(ptr));
self.sort_column = column;
}
fn getRowId(ptr: *anyopaque, index: usize) ?i64 {
const self: *MockProvider = @ptrCast(@alignCast(ptr));
if (index < self.data.len) return self.data[index].id;
return null;
}
fn processBackgroundTasks(_: *anyopaque) bool {
return false;
}
fn invalidate(ptr: *anyopaque) void {
const self: *MockProvider = @ptrCast(@alignCast(ptr));
self.invalidated = true;
}
fn deinitFn(_: *anyopaque) void {}
};
test "DataProvider basic operations" {
const values1 = [_][]const u8{ "1", "Alice" };
const values2 = [_][]const u8{ "2", "Bob" };
const values3 = [_][]const u8{ "3", "Charlie" };
const rows = [_]RowData{
.{ .id = 1, .values = &values1 },
.{ .id = 2, .values = &values2 },
.{ .id = 3, .values = &values3 },
};
var mock = MockProvider{
.data = &rows,
.total = 100,
};
const provider = mock.toDataProvider();
// Test fetchWindow
const window = try provider.fetchWindow(0, 2);
try testing.expectEqual(@as(usize, 2), window.len);
try testing.expectEqual(@as(i64, 1), window[0].id);
try testing.expectEqual(@as(i64, 2), window[1].id);
// Test counts
const total = provider.getTotalCount();
try testing.expectEqual(@as(usize, 100), total.value);
try testing.expectEqual(types.LoadState.ready, total.state);
// Test getRowId
try testing.expectEqual(@as(?i64, 2), provider.getRowId(1));
try testing.expectEqual(@as(?i64, null), provider.getRowId(100));
// Test setFilter
provider.setFilter("test");
try testing.expect(mock.filter_applied);
// Test setSort
provider.setSort("name", .ascending);
try testing.expectEqualStrings("name", mock.sort_column.?);
// Test invalidate
provider.invalidate();
try testing.expect(mock.invalidated);
}
test "DataProvider fetchWindow bounds" {
const values1 = [_][]const u8{"1"};
const rows = [_]RowData{
.{ .id = 1, .values = &values1 },
};
var mock = MockProvider{
.data = &rows,
.total = 1,
};
const provider = mock.toDataProvider();
// Offset beyond data
const empty = try provider.fetchWindow(100, 10);
try testing.expectEqual(@as(usize, 0), empty.len);
// Limit beyond data
const partial = try provider.fetchWindow(0, 100);
try testing.expectEqual(@as(usize, 1), partial.len);
}