diff --git a/src/widgets/virtual_list/data_provider.zig b/src/widgets/virtual_advanced_table/data_provider.zig similarity index 96% rename from src/widgets/virtual_list/data_provider.zig rename to src/widgets/virtual_advanced_table/data_provider.zig index 9966b4a..e4776f2 100644 --- a/src/widgets/virtual_list/data_provider.zig +++ b/src/widgets/virtual_advanced_table/data_provider.zig @@ -1,6 +1,6 @@ //! DataProvider - Interface genérica para fuentes de datos //! -//! Permite que VirtualList trabaje con cualquier fuente de datos +//! Permite que VirtualAdvancedTable trabaje con cualquier fuente de datos //! (SQLite, arrays, APIs, etc.) mediante un vtable pattern. //! //! Ejemplo de implementación: diff --git a/src/widgets/virtual_list/state.zig b/src/widgets/virtual_advanced_table/state.zig similarity index 66% rename from src/widgets/virtual_list/state.zig rename to src/widgets/virtual_advanced_table/state.zig index f793272..971f31d 100644 --- a/src/widgets/virtual_list/state.zig +++ b/src/widgets/virtual_advanced_table/state.zig @@ -1,4 +1,4 @@ -//! Estado del VirtualList +//! Estado del VirtualAdvancedTable //! //! Mantiene el estado de navegación, selección y caché del widget. @@ -7,9 +7,11 @@ const types = @import("types.zig"); const RowData = types.RowData; const CountInfo = types.CountInfo; const SortDirection = types.SortDirection; +const CellId = types.CellId; +const CellGeometry = types.CellGeometry; -/// Estado del widget VirtualList -pub const VirtualListState = struct { +/// Estado del widget VirtualAdvancedTable +pub const VirtualAdvancedTableState = struct { // ========================================================================= // Selección // ========================================================================= @@ -120,6 +122,34 @@ pub const VirtualListState = struct { footer_display_buf: [96]u8 = undefined, footer_display_len: usize = 0, + // ========================================================================= + // Estado de edición CRUD Excel-style + // ========================================================================= + + /// Celda actualmente en edición (null = no editando) + editing_cell: ?CellId = null, + + /// Valor original de la celda (para Escape revertir) + original_value: [256]u8 = undefined, + original_value_len: usize = 0, + + /// Contador de Escapes (1 = revertir celda, 2 = descartar fila) + escape_count: u8 = 0, + + /// Fila actual tiene cambios sin guardar en BD + row_dirty: bool = false, + + /// Última fila editada (para detectar cambio de fila) + last_edited_row: ?usize = null, + + /// Buffer de edición (texto actual en el editor) + edit_buffer: [256]u8 = undefined, + edit_buffer_len: usize = 0, + edit_cursor: usize = 0, + + /// Flag: celda requiere commit al terminar edición + cell_value_changed: bool = false, + const Self = @This(); // ========================================================================= @@ -403,6 +433,192 @@ pub const VirtualListState = struct { pub fn goToEndX(self: *Self, max_scroll: i32) void { self.scroll_offset_x = max_scroll; } + + // ========================================================================= + // Métodos de edición CRUD Excel-style + // ========================================================================= + + /// Verifica si hay una celda en edición + pub fn isEditing(self: *const Self) bool { + return self.editing_cell != null; + } + + /// Inicia edición de una celda + /// initial_char: si viene de tecla alfanumérica, el caracter inicial (null = mostrar valor actual) + pub fn startEditing(self: *Self, cell: CellId, current_value: []const u8, initial_char: ?u8) void { + // Guardar valor original (para Escape) + const len = @min(current_value.len, self.original_value.len); + @memcpy(self.original_value[0..len], current_value[0..len]); + self.original_value_len = len; + + // Inicializar buffer de edición + if (initial_char) |c| { + // Tecla alfanumérica: empezar con ese caracter + self.edit_buffer[0] = c; + self.edit_buffer_len = 1; + self.edit_cursor = 1; + } else { + // Doble-click/Space: mostrar valor actual + @memcpy(self.edit_buffer[0..len], current_value[0..len]); + self.edit_buffer_len = len; + self.edit_cursor = len; + } + + self.editing_cell = cell; + self.escape_count = 0; + self.cell_value_changed = false; + } + + /// Obtiene el texto actual del editor + pub fn getEditText(self: *const Self) []const u8 { + return self.edit_buffer[0..self.edit_buffer_len]; + } + + /// Establece el texto del editor + pub fn setEditText(self: *Self, text: []const u8) void { + const len = @min(text.len, self.edit_buffer.len); + @memcpy(self.edit_buffer[0..len], text[0..len]); + self.edit_buffer_len = len; + self.edit_cursor = len; + } + + /// Obtiene el valor original (antes de editar) + pub fn getOriginalValue(self: *const Self) []const u8 { + return self.original_value[0..self.original_value_len]; + } + + /// Verifica si el valor ha cambiado respecto al original + pub fn hasValueChanged(self: *const Self) bool { + const current = self.getEditText(); + const original = self.getOriginalValue(); + return !std.mem.eql(u8, current, original); + } + + /// Finaliza edición guardando cambios (retorna true si hubo cambios) + pub fn commitEdit(self: *Self) bool { + if (self.editing_cell == null) return false; + + const changed = self.hasValueChanged(); + if (changed) { + self.row_dirty = true; + self.cell_value_changed = true; + } + + // Actualizar última fila editada + self.last_edited_row = self.editing_cell.?.row; + + self.editing_cell = null; + self.escape_count = 0; + return changed; + } + + /// Finaliza edición descartando cambios + pub fn cancelEdit(self: *Self) void { + self.editing_cell = null; + self.escape_count = 0; + self.cell_value_changed = false; + } + + /// Revierte el texto de la celda al valor original (Escape 1) + pub fn revertCellText(self: *Self) void { + const original = self.getOriginalValue(); + @memcpy(self.edit_buffer[0..original.len], original); + self.edit_buffer_len = original.len; + self.edit_cursor = original.len; + } + + /// Maneja la tecla Escape (retorna acción a tomar) + pub const EscapeAction = enum { + /// Texto revertido, mantener edición + reverted, + /// Descartar cambios de fila + discard_row, + /// No estaba editando + none, + }; + + pub fn handleEscape(self: *Self) EscapeAction { + if (self.editing_cell == null) return .none; + + self.escape_count += 1; + + if (self.escape_count == 1) { + // Escape 1: Revertir texto a valor original + self.revertCellText(); + return .reverted; + } else { + // Escape 2+: Descartar cambios de fila + self.cancelEdit(); + self.row_dirty = false; + return .discard_row; + } + } + + /// Verifica si cambió de fila (para auto-save) + pub fn isRowChange(self: *const Self, new_row: usize) bool { + if (self.last_edited_row) |last| { + return last != new_row; + } + return false; + } + + /// Marca la fila como guardada (limpia dirty flag) + pub fn markRowSaved(self: *Self) void { + self.row_dirty = false; + } + + /// Resetea el estado de edición completamente + pub fn resetEditState(self: *Self) void { + self.editing_cell = null; + self.escape_count = 0; + self.row_dirty = false; + self.last_edited_row = null; + self.edit_buffer_len = 0; + self.edit_cursor = 0; + self.cell_value_changed = false; + } + + // ========================================================================= + // Geometría de celdas + // ========================================================================= + + /// Calcula la geometría (posición y tamaño) de una celda visible + /// Retorna null si la celda no está visible en pantalla + pub fn getCellGeometry( + self: *const Self, + row: usize, + col: usize, + columns: []const types.ColumnDef, + row_height: u32, + bounds_x: i32, + bounds_y: i32, + header_height: u32, + filter_bar_height: u32, + ) ?CellGeometry { + // Verificar si la fila está en la ventana visible + if (row < self.scroll_offset) return null; + const visible_row = row - self.scroll_offset; + + // Calcular Y (después de filter bar + header) + const content_start_y = bounds_y + @as(i32, @intCast(filter_bar_height)) + @as(i32, @intCast(header_height)); + const y = content_start_y + @as(i32, @intCast(visible_row * row_height)); + + // Verificar columna válida + if (col >= columns.len) return null; + + // Calcular X (sumando anchos de columnas anteriores, menos scroll horizontal) + var x: i32 = bounds_x - self.scroll_offset_x; + for (columns[0..col]) |c| { + x += @as(i32, @intCast(c.width)); + } + + return CellGeometry{ + .x = x, + .y = y, + .w = columns[col].width, + .h = row_height, + }; + } }; // ============================================================================= @@ -411,8 +627,8 @@ pub const VirtualListState = struct { const testing = std.testing; -test "VirtualListState selection" { - var state = VirtualListState{}; +test "VirtualAdvancedTableState selection" { + var state = VirtualAdvancedTableState{}; // Initial state try testing.expectEqual(@as(?i64, null), state.selected_id); @@ -437,8 +653,8 @@ test "VirtualListState selection" { try testing.expect(state.selection_changed); } -test "VirtualListState filter" { - var state = VirtualListState{}; +test "VirtualAdvancedTableState filter" { + var state = VirtualAdvancedTableState{}; state.setFilter("test"); try testing.expectEqualStrings("test", state.getFilter()); @@ -448,8 +664,8 @@ test "VirtualListState filter" { try testing.expectEqualStrings("", state.getFilter()); } -test "VirtualListState sort" { - var state = VirtualListState{}; +test "VirtualAdvancedTableState sort" { + var state = VirtualAdvancedTableState{}; // Initial: no sort try testing.expectEqual(@as(?[]const u8, null), state.sort_column); @@ -475,8 +691,8 @@ test "VirtualListState sort" { try testing.expectEqual(SortDirection.ascending, state.sort_direction); } -test "VirtualListState window index conversion" { - var state = VirtualListState{}; +test "VirtualAdvancedTableState window index conversion" { + var state = VirtualAdvancedTableState{}; state.window_start = 100; const values = [_][]const u8{"test"}; diff --git a/src/widgets/virtual_list/types.zig b/src/widgets/virtual_advanced_table/types.zig similarity index 89% rename from src/widgets/virtual_list/types.zig rename to src/widgets/virtual_advanced_table/types.zig index 31f5a9f..c1031f8 100644 --- a/src/widgets/virtual_list/types.zig +++ b/src/widgets/virtual_advanced_table/types.zig @@ -1,4 +1,4 @@ -//! Tipos para VirtualList +//! Tipos para VirtualAdvancedTable //! //! Tipos genéricos para listas virtualizadas que trabajan con cualquier //! fuente de datos (WHO, DOC, etc.) @@ -29,6 +29,24 @@ pub const SortDirection = enum { } }; +/// Identificador de celda (fila + columna) +pub const CellId = struct { + row: usize, + col: usize, + + pub fn eql(self: CellId, other: CellId) bool { + return self.row == other.row and self.col == other.col; + } +}; + +/// Geometría de una celda (posición y tamaño en pixels) +pub const CellGeometry = struct { + x: i32, + y: i32, + w: u32, + h: u32, +}; + /// Datos genéricos de una fila /// El DataProvider convierte sus datos específicos a este formato pub const RowData = struct { @@ -152,11 +170,11 @@ pub const FilterBarConfig = struct { }; // ============================================================================= -// VirtualListConfig +// VirtualAdvancedTableConfig // ============================================================================= -/// Configuración del VirtualList -pub const VirtualListConfig = struct { +/// Configuración del VirtualAdvancedTable +pub const VirtualAdvancedTableConfig = struct { /// Altura de cada fila en pixels row_height: u16 = 24, diff --git a/src/widgets/virtual_list/virtual_list.zig b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig similarity index 90% rename from src/widgets/virtual_list/virtual_list.zig rename to src/widgets/virtual_advanced_table/virtual_advanced_table.zig index 806ac75..1f18b21 100644 --- a/src/widgets/virtual_list/virtual_list.zig +++ b/src/widgets/virtual_advanced_table/virtual_advanced_table.zig @@ -1,11 +1,11 @@ -//! VirtualList - Widget de lista virtualizada +//! VirtualAdvancedTable - Widget de lista virtualizada //! //! Lista escalable que solo carga en memoria los registros visibles + buffer. //! Diseñada para trabajar con bases de datos grandes (100k+ registros). //! //! ## Uso //! ```zig -//! const result = virtualList(ctx, rect, &state, provider, .{ +//! const result = virtualAdvancedTable(ctx, rect, &state, provider, .{ //! .columns = &columns, //! .virtualization_threshold = 500, //! }); @@ -31,16 +31,18 @@ pub const ColumnDef = types.ColumnDef; pub const SortDirection = types.SortDirection; pub const LoadState = types.LoadState; pub const CountInfo = types.CountInfo; -pub const VirtualListConfig = types.VirtualListConfig; +pub const VirtualAdvancedTableConfig = types.VirtualAdvancedTableConfig; pub const FilterBarConfig = types.FilterBarConfig; pub const FilterChipDef = types.FilterChipDef; pub const ChipSelectMode = types.ChipSelectMode; +pub const CellId = types.CellId; +pub const CellGeometry = types.CellGeometry; pub const DataProvider = data_provider.DataProvider; -pub const VirtualListState = state_mod.VirtualListState; +pub const VirtualAdvancedTableState = state_mod.VirtualAdvancedTableState; -/// Resultado de renderizar el VirtualList -pub const VirtualListResult = struct { +/// Resultado de renderizar el VirtualAdvancedTable +pub const VirtualAdvancedTableResult = struct { /// La selección cambió este frame selection_changed: bool = false, @@ -81,26 +83,26 @@ pub const VirtualListResult = struct { // Widget principal // ============================================================================= -/// Renderiza un VirtualList -pub fn virtualList( +/// Renderiza un VirtualAdvancedTable +pub fn virtualAdvancedTable( ctx: *Context, - list_state: *VirtualListState, + list_state: *VirtualAdvancedTableState, provider: DataProvider, - config: VirtualListConfig, -) VirtualListResult { + config: VirtualAdvancedTableConfig, +) VirtualAdvancedTableResult { const bounds = ctx.layout.nextRect(); - return virtualListRect(ctx, bounds, list_state, provider, config); + return virtualAdvancedTableRect(ctx, bounds, list_state, provider, config); } -/// Renderiza un VirtualList en un rectángulo específico -pub fn virtualListRect( +/// Renderiza un VirtualAdvancedTable en un rectángulo específico +pub fn virtualAdvancedTableRect( ctx: *Context, bounds: Layout.Rect, - list_state: *VirtualListState, + list_state: *VirtualAdvancedTableState, provider: DataProvider, - config: VirtualListConfig, -) VirtualListResult { - var result = VirtualListResult{}; + config: VirtualAdvancedTableConfig, +) VirtualAdvancedTableResult { + var result = VirtualAdvancedTableResult{}; if (bounds.isEmpty() or config.columns.len == 0) return result; @@ -108,7 +110,7 @@ pub fn virtualListRect( list_state.resetFrameFlags(); // Get colors - const colors = config.colors orelse VirtualListConfig.Colors{}; + const colors = config.colors orelse VirtualAdvancedTableConfig.Colors{}; // Generate unique ID for focus system const widget_id: u64 = @intFromPtr(list_state); @@ -284,7 +286,7 @@ pub fn virtualListRect( // Helper: Check if refetch needed // ============================================================================= -fn needsRefetch(list_state: *const VirtualListState, visible_rows: usize, buffer_size: usize) bool { +fn needsRefetch(list_state: *const VirtualAdvancedTableState, visible_rows: usize, buffer_size: usize) bool { // First load if (list_state.current_window.len == 0) return true; @@ -309,9 +311,9 @@ fn drawFilterBar( ctx: *Context, bounds: Layout.Rect, config: FilterBarConfig, - colors: *const VirtualListConfig.Colors, - list_state: *VirtualListState, - result: *VirtualListResult, + colors: *const VirtualAdvancedTableConfig.Colors, + list_state: *VirtualAdvancedTableState, + result: *VirtualAdvancedTableResult, ) void { const padding: i32 = 6; const chip_h: u32 = 22; @@ -541,10 +543,10 @@ fn drawHeaderAt( ctx: *Context, bounds: Layout.Rect, header_y: i32, - config: VirtualListConfig, - colors: *const VirtualListConfig.Colors, - list_state: *VirtualListState, - result: *VirtualListResult, + config: VirtualAdvancedTableConfig, + colors: *const VirtualAdvancedTableConfig.Colors, + list_state: *VirtualAdvancedTableState, + result: *VirtualAdvancedTableResult, scroll_offset_x: i32, ) void { const header_h = config.row_height; @@ -621,11 +623,11 @@ fn drawHeaderAt( fn drawRows( ctx: *Context, content_bounds: Layout.Rect, - config: VirtualListConfig, - colors: *const VirtualListConfig.Colors, - list_state: *VirtualListState, + config: VirtualAdvancedTableConfig, + colors: *const VirtualAdvancedTableConfig.Colors, + list_state: *VirtualAdvancedTableState, visible_rows: usize, - result: *VirtualListResult, + result: *VirtualAdvancedTableResult, scroll_offset_x: i32, ) void { _ = result; @@ -698,8 +700,8 @@ fn drawRows( fn drawFooter( ctx: *Context, bounds: Layout.Rect, - colors: *const VirtualListConfig.Colors, - list_state: *VirtualListState, + colors: *const VirtualAdvancedTableConfig.Colors, + list_state: *VirtualAdvancedTableState, ) void { // Background ctx.pushCommand(Command.rect( @@ -748,10 +750,10 @@ fn drawScrollbar( bounds: Layout.Rect, header_h: u32, footer_h: u32, - list_state: *VirtualListState, + list_state: *VirtualAdvancedTableState, visible_rows: usize, total_rows: usize, - colors: *const VirtualListConfig.Colors, + colors: *const VirtualAdvancedTableConfig.Colors, ) void { const scrollbar_w: u32 = 12; const content_h = bounds.h -| header_h -| footer_h; @@ -785,7 +787,7 @@ fn drawScrollbarH( scroll_offset_x: i32, max_scroll_x: i32, available_width: u32, - colors: *const VirtualListConfig.Colors, + colors: *const VirtualAdvancedTableConfig.Colors, ) void { const scrollbar_v_w: u32 = 12; // Width of vertical scrollbar area @@ -817,12 +819,12 @@ fn drawScrollbarH( fn handleKeyboard( ctx: *Context, - list_state: *VirtualListState, + list_state: *VirtualAdvancedTableState, provider: DataProvider, visible_rows: usize, total_rows: usize, max_scroll_x: i32, - result: *VirtualListResult, + result: *VirtualAdvancedTableResult, ) void { _ = provider; _ = result; @@ -860,9 +862,9 @@ fn handleMouseClick( bounds: Layout.Rect, filter_bar_h: u32, header_h: u32, - config: VirtualListConfig, - list_state: *VirtualListState, - result: *VirtualListResult, + config: VirtualAdvancedTableConfig, + list_state: *VirtualAdvancedTableState, + result: *VirtualAdvancedTableResult, ) void { _ = result; @@ -892,15 +894,15 @@ fn handleMouseClick( // Tests // ============================================================================= -test "virtual_list module imports" { +test "virtual_advanced_table module imports" { _ = types; _ = data_provider; _ = state_mod; _ = RowData; _ = ColumnDef; _ = DataProvider; - _ = VirtualListState; - _ = VirtualListResult; + _ = VirtualAdvancedTableState; + _ = VirtualAdvancedTableResult; } test { diff --git a/src/widgets/widgets.zig b/src/widgets/widgets.zig index d39c93b..7abbf36 100644 --- a/src/widgets/widgets.zig +++ b/src/widgets/widgets.zig @@ -41,7 +41,7 @@ pub const canvas = @import("canvas.zig"); pub const chart = @import("chart.zig"); pub const icon = @import("icon.zig"); pub const virtual_scroll = @import("virtual_scroll.zig"); -pub const virtual_list = @import("virtual_list/virtual_list.zig"); +pub const virtual_advanced_table = @import("virtual_advanced_table/virtual_advanced_table.zig"); // Gio parity widgets (Phase 1) pub const switch_widget = @import("switch.zig");