zcatgui/src/widgets/virtual_advanced_table/paged_datasource.zig
R.Eugenio b3a33ec4f3 fix(virtual_table): RowIdGetter now handles injected rows
Bug: Tab within injected row was triggering immediate row commit.
This happened because RowIdGetter.getRowId() returned the REAL row ID
for the injected row index, instead of NEW_ROW_ID.

Fix:
- RowIdGetter now takes injected_idx parameter
- Returns NEW_ROW_ID for injected row
- Adjusts indices for rows after injection (row-1)
- num_rows calculation now accounts for injection (+1)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 12:50:30 +01:00

237 lines
9.2 KiB
Zig

//! 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 + inyección)
pub fn getRowCount(self: *Self) usize {
// Usar conteo filtrado si está disponible
const count_info = self.state.getDisplayCount();
var count = count_info.value;
// Si hay una fila inyectada, sumar 1 al conteo visual
if (self.state.injected_row_idx != null) {
count += 1;
}
return count;
}
/// Escribe el valor de una celda en el buffer proporcionado.
/// El row es índice global, se convierte a índice de ventana.
/// Maneja filas inyectadas (Ctrl+N entre líneas).
/// 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 "";
// =====================================================================
// Manejo de fila inyectada
// =====================================================================
if (self.state.injected_row_idx) |inj_idx| {
if (row == inj_idx) {
// Esta es la fila inyectada - leer del edit buffer
if (self.state.row_edit_buffer.getPendingValue(col)) |pending| {
std.debug.print("[PDS-DEBUG] injected row={} col={} pending=\"{s}\"\n", .{ row, col, pending });
const copy_len = @min(pending.len, buf.len);
@memcpy(buf[0..copy_len], pending[0..copy_len]);
return buf[0..copy_len];
}
// Sin valor pendiente - retornar vacío (celda nueva)
std.debug.print("[PDS-DEBUG] injected row={} col={} NO pending\n", .{ row, col });
return "";
} else if (row > inj_idx) {
// Fila después de la inyección - ajustar índice (-1)
return self.getCellValueFromProvider(row - 1, col, buf);
}
// row < inj_idx: continuar normal
}
// Flujo normal (sin inyección o row < inj_idx)
return self.getCellValueFromProvider(row, col, buf);
}
/// Helper interno: obtiene valor de celda del provider (sin ajuste por inyección)
fn getCellValueFromProvider(self: *Self, row: usize, col: usize, buf: []u8) []const u8 {
// Convertir índice global a índice de ventana
const window_idx = self.state.globalToWindowIndex(row) orelse {
// Fila fuera de ventana actual - retornar vacío
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.
/// Maneja filas inyectadas (Ctrl+N entre líneas).
pub fn getRowId(self: *Self, row: usize) i64 {
// Manejo de fila inyectada
if (self.state.injected_row_idx) |inj_idx| {
if (row == inj_idx) {
// Esta es la fila inyectada
return self.state.injected_row_id;
} else if (row > inj_idx) {
// Fila después de la inyección - ajustar índice
return self.getRowIdFromProvider(row - 1);
}
}
// Flujo normal
return self.getRowIdFromProvider(row);
}
/// Helper interno: obtiene ID de fila del provider (sin ajuste por inyección)
fn getRowIdFromProvider(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);
}